diff options
Diffstat (limited to 'src/animation/backend')
-rw-r--r-- | src/animation/backend/animationclip.cpp | 58 | ||||
-rw-r--r-- | src/animation/backend/animationutils.cpp | 103 | ||||
-rw-r--r-- | src/animation/backend/animationutils_p.h | 35 | ||||
-rw-r--r-- | src/animation/backend/backend.pri | 6 | ||||
-rw-r--r-- | src/animation/backend/blendedclipanimator.cpp | 27 | ||||
-rw-r--r-- | src/animation/backend/blendedclipanimator_p.h | 13 | ||||
-rw-r--r-- | src/animation/backend/clipanimator.cpp | 23 | ||||
-rw-r--r-- | src/animation/backend/clipanimator_p.h | 12 | ||||
-rw-r--r-- | src/animation/backend/evaluateblendclipanimatorjob.cpp | 30 | ||||
-rw-r--r-- | src/animation/backend/evaluateblendclipanimatorjob_p.h | 3 | ||||
-rw-r--r-- | src/animation/backend/evaluateclipanimatorjob.cpp | 14 | ||||
-rw-r--r-- | src/animation/backend/evaluateclipanimatorjob_p.h | 3 | ||||
-rw-r--r-- | src/animation/backend/fcurve_p.h | 25 | ||||
-rw-r--r-- | src/animation/backend/gltfimporter.cpp | 852 | ||||
-rw-r--r-- | src/animation/backend/gltfimporter_p.h | 232 |
15 files changed, 1344 insertions, 92 deletions
diff --git a/src/animation/backend/animationclip.cpp b/src/animation/backend/animationclip.cpp index daee4008d..e8f8f4552 100644 --- a/src/animation/backend/animationclip.cpp +++ b/src/animation/backend/animationclip.cpp @@ -41,6 +41,7 @@ #include <Qt3DAnimation/private/qanimationcliploader_p.h> #include <Qt3DAnimation/private/animationlogging_p.h> #include <Qt3DAnimation/private/managers_p.h> +#include <Qt3DAnimation/private/gltfimporter_p.h> #include <Qt3DRender/private/qurlhelper_p.h> #include <Qt3DCore/qpropertyupdatedchange.h> @@ -209,28 +210,43 @@ void AnimationClip::loadAnimationFromUrl() return; } - QByteArray animationData = file.readAll(); - QJsonDocument document = QJsonDocument::fromJson(animationData); - QJsonObject rootObject = document.object(); - - // TODO: Allow loading of a named animation from a file containing many - QJsonArray animationsArray = rootObject[QLatin1String("animations")].toArray(); - qCDebug(Jobs) << "Found" << animationsArray.size() << "animations:"; - for (int i = 0; i < animationsArray.size(); ++i) { - QJsonObject animation = animationsArray.at(i).toObject(); - qCDebug(Jobs) << "Animation Name:" << animation[QLatin1String("animationName")].toString(); - } + // TODO: Convert to plugins + // Load glTF or "native" + if (filePath.endsWith(QLatin1String("gltf"))) { + qCDebug(Jobs) << "Loading glTF animation from" << filePath; + GLTFImporter gltf; + gltf.load(&file); + // TODO: Allow loading of a named animation from a file containing many + m_name = gltf.animations().first().name; + m_channels = gltf.createAnimationData(); + } else if (filePath.endsWith(QLatin1String("json"))) { + // Native format + QByteArray animationData = file.readAll(); + QJsonDocument document = QJsonDocument::fromJson(animationData); + QJsonObject rootObject = document.object(); + + // TODO: Allow loading of a named animation from a file containing many + QJsonArray animationsArray = rootObject[QLatin1String("animations")].toArray(); + qCDebug(Jobs) << "Found" << animationsArray.size() << "animations:"; + for (int i = 0; i < animationsArray.size(); ++i) { + QJsonObject animation = animationsArray.at(i).toObject(); + qCDebug(Jobs) << "Animation Name:" << animation[QLatin1String("animationName")].toString(); + } - // For now just load the first animation - // TODO: Allow loading a named animation from within the file analogous to QMesh - QJsonObject animation = animationsArray.at(0).toObject(); - m_name = animation[QLatin1String("animationName")].toString(); - QJsonArray channelsArray = animation[QLatin1String("channels")].toArray(); - const int channelCount = channelsArray.size(); - m_channels.resize(channelCount); - for (int i = 0; i < channelCount; ++i) { - const QJsonObject group = channelsArray.at(i).toObject(); - m_channels[i].read(group); + // For now just load the first animation + // TODO: Allow loading a named animation from within the file analogous to QMesh + QJsonObject animation = animationsArray.at(0).toObject(); + m_name = animation[QLatin1String("animationName")].toString(); + QJsonArray channelsArray = animation[QLatin1String("channels")].toArray(); + const int channelCount = channelsArray.size(); + m_channels.resize(channelCount); + for (int i = 0; i < channelCount; ++i) { + const QJsonObject group = channelsArray.at(i).toObject(); + m_channels[i].read(group); + } + } else { + qWarning() << "Unknown animation clip type. Please use json or glTF 2.0"; + setStatus(QAnimationClipLoader::Error); } } diff --git a/src/animation/backend/animationutils.cpp b/src/animation/backend/animationutils.cpp index d05d96f38..fb5b19a5d 100644 --- a/src/animation/backend/animationutils.cpp +++ b/src/animation/backend/animationutils.cpp @@ -93,22 +93,26 @@ ClipEvaluationData evaluationDataForClip(AnimationClip *clip, { // global time values expected in seconds ClipEvaluationData result; - result.localTime = localTimeFromGlobalTime(animatorData.globalTime, animatorData.startTime, - animatorData.playbackRate, clip->duration(), - animatorData.loopCount, result.currentLoop); + result.currentLoop = animatorData.currentLoop; + result.localTime = localTimeFromElapsedTime(animatorData.currentTime, animatorData.elapsedTime, + animatorData.playbackRate, clip->duration(), + animatorData.loopCount, result.currentLoop); result.isFinalFrame = isFinalFrame(result.localTime, clip->duration(), result.currentLoop, animatorData.loopCount); return result; } -double localTimeFromGlobalTime(double t_global, - double t_start_global, - double playbackRate, - double duration, - int loopCount, - int ¤tLoop) +double localTimeFromElapsedTime(double t_current_local, + double t_elapsed_global, + double playbackRate, + double duration, + int loopCount, + int ¤tLoop) { - double t_local = playbackRate * (t_global - t_start_global); + // Calculate the new local time. + // playhead + rate * dt + // where playhead is completed loops * duration + current loop local time + double t_local = currentLoop * duration + t_current_local + playbackRate * t_elapsed_global; double loopNumber = 0; if (loopCount == 1) { t_local = qBound(0.0, t_local, duration); @@ -123,31 +127,44 @@ double localTimeFromGlobalTime(double t_global, t_local = std::fmod(t_local, duration); // Ensure we clamp to end of final loop - if (loopNumber == loopCount) { + if (int(loopNumber) == loopCount) { loopNumber = loopCount - 1; t_local = duration; } } - qCDebug(Jobs) << "t_global - t_start =" << t_global - t_start_global - << "current loop =" << loopNumber + qCDebug(Jobs) << "current loop =" << loopNumber << "t =" << t_local << "duration =" << duration; - currentLoop = loopNumber; + currentLoop = int(loopNumber); return t_local; } -double phaseFromGlobalTime(double t_global, double t_start_global, - double playbackRate, double duration, - int loopCount, int ¤tLoop) +double phaseFromElapsedTime(double t_current_local, + double t_elapsed_global, + double playbackRate, + double duration, + int loopCount, + int ¤tLoop) { - const double t_local = localTimeFromGlobalTime(t_global, t_start_global, playbackRate, - duration, loopCount, currentLoop); + const double t_local = localTimeFromElapsedTime(t_current_local, t_elapsed_global, playbackRate, + duration, loopCount, currentLoop); return t_local / duration; } +/*! + \internal + + Calculates the indices required to map from the component ordering within the + provided \a channel, into the standard channel orderings expected by Qt types. + + For example, given a channel representing a rotation with the components ordered + as X, Y, Z, Y, this function will return the indices [3, 0, 1, 2] which can then + later be used as part of the format vector in the formatClipResults() function to + remap the channels into the standard W, X, Y, Z order required by QQuaternion. +*/ ComponentIndices channelComponentsToIndices(const Channel &channel, int dataType, int offset) { #if defined Q_COMPILER_UNIFORM_INIT @@ -171,9 +188,9 @@ ComponentIndices channelComponentsToIndices(const Channel &channel, int dataType } ComponentIndices channelComponentsToIndicesHelper(const Channel &channel, - int dataType, - int offset, - const QVector<char> &suffixes) + int dataType, + int offset, + const QVector<char> &suffixes) { const int expectedComponentCount = componentsForType(dataType); const int actualComponentCount = channel.channelComponents.size(); @@ -183,21 +200,37 @@ ComponentIndices channelComponentsToIndicesHelper(const Channel &channel, } ComponentIndices indices(expectedComponentCount); + + // Generate the set of channel suffixes + QVector<char> channelSuffixes; + channelSuffixes.reserve(expectedComponentCount); for (int i = 0; i < expectedComponentCount; ++i) { const QString &componentName = channel.channelComponents[i].name; + // An unset component name indicates that the no mapping is necessary // and the index can be used as-is. if (componentName.isEmpty()) { indices[i] = i + offset; continue; } - char suffix = componentName.at(componentName.length() - 1).toLatin1(); - int index = suffixes.indexOf(suffix); + + char channelSuffix = componentName.at(componentName.length() - 1).toLatin1(); + channelSuffixes.push_back(channelSuffix); + } + + // We can short-circuit if the channels were all unnamed (in order) + if (channelSuffixes.isEmpty()) + return indices; + + // Find index of standard index in channel indexes + for (int i = 0; i < expectedComponentCount; ++i) { + int index = channelSuffixes.indexOf(suffixes[i]); if (index != -1) indices[i] = index + offset; else indices[i] = -1; } + return indices; } @@ -428,6 +461,11 @@ QVector<MappingData> buildPropertyMappings(const QVector<ChannelMapping*> &chann const ChannelNameAndType nameAndType = { mapping->channelName(), mapping->type() }; const int index = channelNamesAndTypes.indexOf(nameAndType); if (index != -1) { + // Do we have any animation data for this channel? If not, don't bother + // adding a mapping for it. + if (channelComponentIndices[index].isEmpty()) + continue; + // We got one! mappingData.channelIndices = channelComponentIndices[index]; mappingDataVec.push_back(mappingData); @@ -642,16 +680,14 @@ ComponentIndices generateClipFormatIndices(const QVector<ChannelNameAndType> &ta const int componentCount = targetIndices[i].size(); if (clipChannelIndex != -1) { - // Found a matching channel in the clip. Get the base channel - // component index and populate the format indices for this channel. + // Found a matching channel in the clip. Populate the corresponding + // entries in the format vector with the *source indices* + // needed to build the formatted results. const int baseIndex = clip->channelComponentBaseIndex(clipChannelIndex); - - // Within this group, match channel names with index ordering const auto channelIndices = channelComponentsToIndices(clip->channels()[clipChannelIndex], targetChannel.type, baseIndex); std::copy(channelIndices.begin(), channelIndices.end(), formatIt); - } else { // No such channel in this clip. We'll use default values when // mapping from the clip to the formatted clip results. @@ -673,11 +709,16 @@ ClipResults formatClipResults(const ClipResults &rawClipResults, ClipResults formattedClipResults(elementCount); // Perform a gather operation to format the data + // TODO: For large numbers of components do this in parallel with // for e.g. a parallel_for() like construct + // TODO: We could potentially avoid having holes in these intermediate + // vectors by adjusting the component indices stored in the MappingData + // and format vectors. Needs careful investigation! for (int i = 0; i < elementCount; ++i) { - const float value = format[i] != -1 ? rawClipResults[format[i]] : 0.0f; - formattedClipResults[i] = value; + if (format[i] == -1) + continue; + formattedClipResults[i] = rawClipResults[format[i]]; } return formattedClipResults; diff --git a/src/animation/backend/animationutils_p.h b/src/animation/backend/animationutils_p.h index 4bd3ee64b..d8127d8af 100644 --- a/src/animation/backend/animationutils_p.h +++ b/src/animation/backend/animationutils_p.h @@ -54,6 +54,8 @@ #include <Qt3DCore/qnodeid.h> #include <Qt3DCore/qscenechange.h> +#include <QtCore/qdebug.h> + QT_BEGIN_NAMESPACE namespace Qt3DAnimation { @@ -89,11 +91,25 @@ struct MappingData ComponentIndices channelIndices; }; +#ifndef QT_NO_DEBUG_STREAM +inline QDebug operator<<(QDebug dbg, const MappingData &mapping) +{ + QDebugStateSaver saver(dbg); + dbg << "targetId =" << mapping.targetId << endl + << "jointIndex =" << mapping.jointIndex << endl + << "jointTransformComponent: " << mapping.jointTransformComponent << endl + << "propertyName:" << mapping.propertyName << endl + << "channelIndices:" << mapping.channelIndices; + return dbg; +} +#endif + struct AnimatorEvaluationData { - double globalTime; - double startTime; + double elapsedTime; + double currentTime; int loopCount; + int currentLoop; double playbackRate; }; @@ -142,14 +158,15 @@ struct AnimationCallbackAndValue }; template<typename Animator> -AnimatorEvaluationData evaluationDataForAnimator(Animator animator, Clock* clock, qint64 globalTime) +AnimatorEvaluationData evaluationDataForAnimator(Animator animator, Clock* clock, qint64 nsSincePreviousFrame) { AnimatorEvaluationData data; data.loopCount = animator->loops(); + data.currentLoop = animator->currentLoop(); data.playbackRate = clock != nullptr ? clock->playbackRate() : 1.0; // Convert global time from nsec to sec - data.startTime = double(animator->startTime()) / 1.0e9; - data.globalTime = double(globalTime) / 1.0e9; + data.elapsedTime = double(nsSincePreviousFrame) / 1.0e9; + data.currentTime = animator->lastLocalTime(); return data; } @@ -212,12 +229,12 @@ Q_AUTOTEST_EXPORT QVector<ComponentIndices> assignChannelComponentIndices(const QVector<ChannelNameAndType> &namesAndTypes); Q_AUTOTEST_EXPORT -double localTimeFromGlobalTime(double t_global, double t_start_global, - double playbackRate, double duration, - int loopCount, int ¤tLoop); +double localTimeFromElapsedTime(double t_current_local, double t_elapsed_global, + double playbackRate, double duration, + int loopCount, int ¤tLoop); Q_AUTOTEST_EXPORT -double phaseFromGlobalTime(double t_global, double t_start_global, +double phaseFromElapsedTime(double t_current_local, double t_elapsed_global, double playbackRate, double duration, int loopCount, int ¤tLoop); diff --git a/src/animation/backend/backend.pri b/src/animation/backend/backend.pri index ee36a98ea..cc1104102 100644 --- a/src/animation/backend/backend.pri +++ b/src/animation/backend/backend.pri @@ -28,7 +28,8 @@ HEADERS += \ $$PWD/clipblendvalue_p.h \ $$PWD/animationclip_p.h \ $$PWD/clock_p.h \ - $$PWD/skeleton_p.h + $$PWD/skeleton_p.h \ + $$PWD/gltfimporter_p.h SOURCES += \ $$PWD/handler.cpp \ @@ -54,4 +55,5 @@ SOURCES += \ $$PWD/clipblendvalue.cpp \ $$PWD/animationclip.cpp \ $$PWD/clock.cpp \ - $$PWD/skeleton.cpp + $$PWD/skeleton.cpp \ + $$PWD/gltfimporter.cpp diff --git a/src/animation/backend/blendedclipanimator.cpp b/src/animation/backend/blendedclipanimator.cpp index ec0a5027a..1487d6c3e 100644 --- a/src/animation/backend/blendedclipanimator.cpp +++ b/src/animation/backend/blendedclipanimator.cpp @@ -48,7 +48,8 @@ namespace Animation { BlendedClipAnimator::BlendedClipAnimator() : BackendNode(ReadWrite) , m_running(false) - , m_startGlobalTime(0) + , m_lastGlobalTimeNS(0) + , m_lastLocalTime(0.0) , m_currentLoop(0) , m_loops(1) { @@ -66,6 +67,26 @@ void BlendedClipAnimator::initializeFromPeer(const Qt3DCore::QNodeCreatedChangeB setDirty(Handler::BlendedClipAnimatorDirty); } +double BlendedClipAnimator::lastLocalTime() const +{ + return m_lastLocalTime; +} + +void BlendedClipAnimator::setLastLocalTime(double lastLocalTime) +{ + m_lastLocalTime = lastLocalTime; +} + +void BlendedClipAnimator::setLastGlobalTimeNS(const qint64 &lastGlobalTimeNS) +{ + m_lastGlobalTimeNS = lastGlobalTimeNS; +} + +qint64 BlendedClipAnimator::nsSincePreviousFrame(qint64 currentGlobalTimeNS) +{ + return currentGlobalTimeNS - m_lastGlobalTimeNS; +} + void BlendedClipAnimator::cleanup() { setEnabled(false); @@ -74,7 +95,8 @@ void BlendedClipAnimator::cleanup() m_mapperId = Qt3DCore::QNodeId(); m_clockId = Qt3DCore::QNodeId(); m_running = false; - m_startGlobalTime = 0; + m_lastGlobalTimeNS = 0; + m_lastLocalTime = 0.0; m_currentLoop = 0; m_loops = 1; } @@ -124,6 +146,7 @@ void BlendedClipAnimator::sendCallbacks(const QVector<AnimationCallbackAndValue> } } + Qt3DCore::QNodeId BlendedClipAnimator::blendTreeRootId() const { return m_blendTreeRootId; diff --git a/src/animation/backend/blendedclipanimator_p.h b/src/animation/backend/blendedclipanimator_p.h index 52ebdcc30..79c6d8c43 100644 --- a/src/animation/backend/blendedclipanimator_p.h +++ b/src/animation/backend/blendedclipanimator_p.h @@ -79,8 +79,7 @@ public: void setClockId(Qt3DCore::QNodeId clockId); void setRunning(bool running); - void setStartTime(qint64 globalTime) { m_startGlobalTime = globalTime; } - qint64 startTime() const { return m_startGlobalTime; } + void setStartTime(qint64 globalTime) { m_lastGlobalTimeNS = globalTime; } void setLoops(int loops) { m_loops = loops; } int loops() const { return m_loops; } @@ -96,6 +95,12 @@ public: void animationClipMarkedDirty() { setDirty(Handler::BlendedClipAnimatorDirty); } + qint64 nsSincePreviousFrame(qint64 currentGlobalTimeNS); + void setLastGlobalTimeNS(const qint64 &lastGlobalTimeNS); + + double lastLocalTime() const; + void setLastLocalTime(double lastLocalTime); + private: void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) final; Qt3DCore::QNodeId m_blendTreeRootId; @@ -103,7 +108,9 @@ private: Qt3DCore::QNodeId m_clockId; bool m_running; - qint64 m_startGlobalTime; + qint64 m_lastGlobalTimeNS; + double m_lastLocalTime; + int m_currentLoop; int m_loops; diff --git a/src/animation/backend/clipanimator.cpp b/src/animation/backend/clipanimator.cpp index 568e2dbb0..21c08b80a 100644 --- a/src/animation/backend/clipanimator.cpp +++ b/src/animation/backend/clipanimator.cpp @@ -56,7 +56,8 @@ ClipAnimator::ClipAnimator() , m_clockId() , m_running(false) , m_loops(1) - , m_startGlobalTime(0) + , m_lastGlobalTimeNS(0) + , m_lastLocalTime(0.0) , m_mappingData() , m_currentLoop(0) { @@ -162,6 +163,26 @@ void ClipAnimator::sendCallbacks(const QVector<AnimationCallbackAndValue> &callb } } +qint64 ClipAnimator::nsSincePreviousFrame(qint64 currentGlobalTimeNS) +{ + return currentGlobalTimeNS - m_lastGlobalTimeNS; +} + +void ClipAnimator::setLastGlobalTimeNS(qint64 lastGlobalTimeNS) +{ + m_lastGlobalTimeNS = lastGlobalTimeNS; +} + +double ClipAnimator::lastLocalTime() const +{ + return m_lastLocalTime; +} + +void ClipAnimator::setLastLocalTime(double lastLocalTime) +{ + m_lastLocalTime = lastLocalTime; +} + } // namespace Animation } // namespace Qt3DAnimation diff --git a/src/animation/backend/clipanimator_p.h b/src/animation/backend/clipanimator_p.h index ca05afdd3..4c33819f5 100644 --- a/src/animation/backend/clipanimator_p.h +++ b/src/animation/backend/clipanimator_p.h @@ -86,8 +86,7 @@ public: void setMappingData(const QVector<MappingData> &mappingData) { m_mappingData = mappingData; } QVector<MappingData> mappingData() const { return m_mappingData; } - void setStartTime(qint64 globalTime) { m_startGlobalTime = globalTime; } - qint64 startTime() const { return m_startGlobalTime; } + void setStartTime(qint64 globalTime) { m_lastGlobalTimeNS = globalTime; } int currentLoop() const { return m_currentLoop; } void setCurrentLoop(int currentLoop) { m_currentLoop = currentLoop; } @@ -100,6 +99,12 @@ public: void setFormatIndices(const ComponentIndices &formatIndices) { m_formatIndices = formatIndices; } ComponentIndices formatIndices() const { return m_formatIndices; } + qint64 nsSincePreviousFrame(qint64 currentGlobalTimeNS); + void setLastGlobalTimeNS(qint64 lastGlobalTimeNS); + + double lastLocalTime() const; + void setLastLocalTime(double lastLocalTime); + private: void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) final; @@ -110,7 +115,8 @@ private: int m_loops; // Working state - qint64 m_startGlobalTime; + qint64 m_lastGlobalTimeNS; + double m_lastLocalTime; QVector<MappingData> m_mappingData; int m_currentLoop; diff --git a/src/animation/backend/evaluateblendclipanimatorjob.cpp b/src/animation/backend/evaluateblendclipanimatorjob.cpp index be05f7ec0..3cde7b32f 100644 --- a/src/animation/backend/evaluateblendclipanimatorjob.cpp +++ b/src/animation/backend/evaluateblendclipanimatorjob.cpp @@ -51,6 +51,8 @@ namespace Animation { EvaluateBlendClipAnimatorJob::EvaluateBlendClipAnimatorJob() : Qt3DCore::QAspectJob() + , m_currentGlobalTime(0.0) + , m_lastGlobalTime(0.0) { SET_JOB_RUN_STAT_TYPE(this, JobTypes::EvaluateBlendClipAnimator, 0); } @@ -62,7 +64,15 @@ void EvaluateBlendClipAnimatorJob::run() // TODO: We should be able to cache this for each blend animator and only // update when a node indicates its dependencies have changed as a result // of blend factors changing + BlendedClipAnimator *blendedClipAnimator = m_handler->blendedClipAnimatorManager()->data(m_blendClipAnimatorHandle); + Q_ASSERT(blendedClipAnimator); + if (!blendedClipAnimator->isRunning()) + return; + + qint64 globalTimeNS = m_handler->simulationTime(); + qint64 nsSincePreviousFrame = blendedClipAnimator->nsSincePreviousFrame(globalTimeNS); + Qt3DCore::QNodeId blendTreeRootId = blendedClipAnimator->blendTreeRootId(); const QVector<Qt3DCore::QNodeId> valueNodeIdsToEvaluate = gatherValueNodesToEvaluate(m_handler, blendTreeRootId); @@ -75,15 +85,12 @@ void EvaluateBlendClipAnimatorJob::run() Clock *clock = m_handler->clockManager()->lookupResource(blendedClipAnimator->clockId()); // Calculate the phase given the blend tree duration and global time - const qint64 globalTime = m_handler->simulationTime(); - const AnimatorEvaluationData animatorData = evaluationDataForAnimator(blendedClipAnimator, clock, globalTime); - int currentLoop = 0; - const double phase = phaseFromGlobalTime(animatorData.globalTime, - animatorData.startTime, - animatorData.playbackRate, - duration, - animatorData.loopCount, - currentLoop); + AnimatorEvaluationData animatorData = evaluationDataForAnimator(blendedClipAnimator, clock, nsSincePreviousFrame); + const double phase = phaseFromElapsedTime(animatorData.currentTime, animatorData.elapsedTime, + animatorData.playbackRate, + duration, + animatorData.loopCount, + animatorData.currentLoop); // Iterate over the value nodes of the blend tree, evaluate the // contained animation clips at the current phase and store the results @@ -107,7 +114,10 @@ void EvaluateBlendClipAnimatorJob::run() ClipResults blendedResults = evaluateBlendTree(m_handler, blendedClipAnimator, blendTreeRootId); const double localTime = phase * duration; - const bool finalFrame = isFinalFrame(localTime, duration, currentLoop, animatorData.loopCount); + blendedClipAnimator->setLastGlobalTimeNS(globalTimeNS); + blendedClipAnimator->setLastLocalTime(localTime); + blendedClipAnimator->setCurrentLoop(animatorData.currentLoop); + const bool finalFrame = isFinalFrame(localTime, duration, animatorData.currentLoop, animatorData.loopCount); // Prepare the property change events const QVector<MappingData> mappingData = blendedClipAnimator->mappingData(); diff --git a/src/animation/backend/evaluateblendclipanimatorjob_p.h b/src/animation/backend/evaluateblendclipanimatorjob_p.h index 7548168e9..6ef3b93cf 100644 --- a/src/animation/backend/evaluateblendclipanimatorjob_p.h +++ b/src/animation/backend/evaluateblendclipanimatorjob_p.h @@ -78,6 +78,9 @@ protected: private: HBlendedClipAnimator m_blendClipAnimatorHandle; Handler *m_handler; + + qint64 m_currentGlobalTime; + qint64 m_lastGlobalTime; }; typedef QSharedPointer<EvaluateBlendClipAnimatorJob> EvaluateBlendClipAnimatorJobPtr; diff --git a/src/animation/backend/evaluateclipanimatorjob.cpp b/src/animation/backend/evaluateclipanimatorjob.cpp index d9600c375..972762033 100644 --- a/src/animation/backend/evaluateclipanimatorjob.cpp +++ b/src/animation/backend/evaluateclipanimatorjob.cpp @@ -48,6 +48,8 @@ namespace Animation { EvaluateClipAnimatorJob::EvaluateClipAnimatorJob() : Qt3DCore::QAspectJob() + , m_currentGlobalTime(0.0) + , m_lastGlobalTime(0.0) { SET_JOB_RUN_STAT_TYPE(this, JobTypes::EvaluateClipAnimator, 0); } @@ -56,11 +58,13 @@ void EvaluateClipAnimatorJob::run() { Q_ASSERT(m_handler); - const qint64 globalTime = m_handler->simulationTime(); - //qDebug() << Q_FUNC_INFO << "t_global =" << globalTime; - ClipAnimator *clipAnimator = m_handler->clipAnimatorManager()->data(m_clipAnimatorHandle); Q_ASSERT(clipAnimator); + if (!clipAnimator->isRunning()) + return; + + qint64 globalTimeNS = m_handler->simulationTime(); + qint64 nsSincePreviousFrame = clipAnimator->nsSincePreviousFrame(globalTimeNS); Clock *clock = m_handler->clockManager()->lookupResource(clipAnimator->clockId()); @@ -68,7 +72,7 @@ void EvaluateClipAnimatorJob::run() AnimationClip *clip = m_handler->animationClipLoaderManager()->lookupResource(clipAnimator->clipId()); Q_ASSERT(clip); // Prepare for evaluation (convert global time to local time ....) - const AnimatorEvaluationData animatorEvaluationData = evaluationDataForAnimator(clipAnimator, clock, globalTime); + const AnimatorEvaluationData animatorEvaluationData = evaluationDataForAnimator(clipAnimator, clock, nsSincePreviousFrame); const ClipEvaluationData preEvaluationDataForClip = evaluationDataForClip(clip, animatorEvaluationData); const ClipResults rawClipResults = evaluateClipAtLocalTime(clip, preEvaluationDataForClip.localTime); @@ -80,6 +84,8 @@ void EvaluateClipAnimatorJob::run() clipAnimator->setRunning(false); clipAnimator->setCurrentLoop(preEvaluationDataForClip.currentLoop); + clipAnimator->setLastGlobalTimeNS(globalTimeNS); + clipAnimator->setLastLocalTime(preEvaluationDataForClip.localTime); // Prepare property changes (if finalFrame it also prepares the change for the running property for the frontend) const QVector<Qt3DCore::QSceneChangePtr> changes = preparePropertyChanges(clipAnimator->peerId(), diff --git a/src/animation/backend/evaluateclipanimatorjob_p.h b/src/animation/backend/evaluateclipanimatorjob_p.h index c9b1a8f96..389d12456 100644 --- a/src/animation/backend/evaluateclipanimatorjob_p.h +++ b/src/animation/backend/evaluateclipanimatorjob_p.h @@ -82,6 +82,9 @@ protected: private: HClipAnimator m_clipAnimatorHandle; Handler *m_handler; + + qint64 m_currentGlobalTime; + qint64 m_lastGlobalTime; }; } // namespace Animation diff --git a/src/animation/backend/fcurve_p.h b/src/animation/backend/fcurve_p.h index f2148d1e9..8c5cdd54d 100644 --- a/src/animation/backend/fcurve_p.h +++ b/src/animation/backend/fcurve_p.h @@ -98,14 +98,27 @@ private: inline QDebug operator<<(QDebug dbg, const FCurve &fcurve) { QDebugStateSaver saver(dbg); - dbg << "Keyframe Count =" << fcurve.keyframeCount() << endl; + dbg << "Keyframe Count = " << fcurve.keyframeCount() << endl; for (int i = 0; i < fcurve.keyframeCount(); ++i) { const Keyframe &kf = fcurve.keyframe(i); - dbg << "t =" << fcurve.localTime(i) - << "value =" << kf.value - << "leftHandle =" << kf.leftControlPoint - << "rightHandle =" << kf.rightControlPoint - << endl; + switch (kf.interpolation) { + case QKeyFrame::BezierInterpolation: { + dbg << "t = " << fcurve.localTime(i) + << ", value = " << kf.value + << ", leftHandle = " << kf.leftControlPoint + << ", rightHandle = " << kf.rightControlPoint + << endl; + break; + } + + case QKeyFrame::ConstantInterpolation: + case QKeyFrame::LinearInterpolation: { + dbg << "t = " << fcurve.localTime(i) + << ", value = " << kf.value + << endl; + break; + } + } } return dbg; } diff --git a/src/animation/backend/gltfimporter.cpp b/src/animation/backend/gltfimporter.cpp new file mode 100644 index 000000000..8d9b5bb60 --- /dev/null +++ b/src/animation/backend/gltfimporter.cpp @@ -0,0 +1,852 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "gltfimporter_p.h" +#include <Qt3DAnimation/private/animationlogging_p.h> +#include <Qt3DAnimation/private/fcurve_p.h> +#include <Qt3DAnimation/private/keyframe_p.h> + +#include <QtGui/qopengl.h> +#include <QtGui/qquaternion.h> +#include <QtGui/qvector2d.h> +#include <QtGui/qvector3d.h> +#include <QtGui/qvector4d.h> +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qiodevice.h> +#include <QtCore/qversionnumber.h> + +QT_BEGIN_NAMESPACE + +namespace Qt3DAnimation { +namespace Animation { + +namespace { + +QString gltfTargetPropertyToChannelName(const QString &propertyName) +{ + if (propertyName == QLatin1String("rotation")) + return QLatin1String("Rotation"); + else if (propertyName == QLatin1String("translation")) + return QLatin1String("Location"); + else if (propertyName == QLatin1String("scale")) + return QLatin1String("Scale"); + + qWarning() << "Unknown target property name"; + return QString(); +} + +QKeyFrame::InterpolationType gltfToQKeyFrameInterpolation(GLTFImporter::Sampler::InterpolationMode mode) +{ + switch (mode) { + case GLTFImporter::Sampler::Linear: + return QKeyFrame::LinearInterpolation; + case GLTFImporter::Sampler::Step: + return QKeyFrame::ConstantInterpolation; + case GLTFImporter::Sampler::CubicSpline: + return QKeyFrame::BezierInterpolation; + case GLTFImporter::Sampler::CatmullRomSpline: + // TODO: Implement this interpolation type + qWarning() << "Unhandled interpolation type"; + return QKeyFrame::LinearInterpolation; + } + + return QKeyFrame::LinearInterpolation; +} + +void jsonArrayToSqt(const QJsonArray &jsonArray, Qt3DCore::Sqt &sqt) +{ + Q_ASSERT(jsonArray.size() == 16); + QMatrix4x4 m; + float *data = m.data(); + int i = 0; + for (const auto element : jsonArray) + *(data + i++) = static_cast<float>(element.toDouble()); + + decomposeQMatrix4x4(m, sqt); +} + +void jsonArrayToVector3D(const QJsonArray &jsonArray, QVector3D &v) +{ + Q_ASSERT(jsonArray.size() == 3); + v.setX(static_cast<float>(jsonArray.at(0).toDouble())); + v.setY(static_cast<float>(jsonArray.at(1).toDouble())); + v.setZ(static_cast<float>(jsonArray.at(2).toDouble())); +} + +void jsonArrayToQuaternion(const QJsonArray &jsonArray, QQuaternion &q) +{ + Q_ASSERT(jsonArray.size() == 4); + q.setX(static_cast<float>(jsonArray.at(0).toDouble())); + q.setY(static_cast<float>(jsonArray.at(1).toDouble())); + q.setZ(static_cast<float>(jsonArray.at(2).toDouble())); + q.setScalar(static_cast<float>(jsonArray.at(3).toDouble())); +} + +} + +#define KEY_ACCESSORS QLatin1String("accessors") +#define KEY_ANIMATIONS QLatin1String("animations") +#define KEY_ASSET QLatin1String("asset") +#define KEY_BUFFER QLatin1String("buffer") +#define KEY_BUFFERS QLatin1String("buffers") +#define KEY_BUFFER_VIEW QLatin1String("bufferView") +#define KEY_BUFFER_VIEWS QLatin1String("bufferViews") +#define KEY_BYTE_LENGTH QLatin1String("byteLength") +#define KEY_BYTE_OFFSET QLatin1String("byteOffset") +#define KEY_BYTE_STRIDE QLatin1String("byteStride") +#define KEY_CAMERA QLatin1String("camera") +#define KEY_CHANNELS QLatin1String("channels") +#define KEY_CHILDREN QLatin1String("children") +#define KEY_COMPONENT_TYPE QLatin1String("componentType") +#define KEY_COUNT QLatin1String("count") +#define KEY_JOINTS QLatin1String("joints") +#define KEY_INPUT QLatin1String("input") +#define KEY_INTERPOLATION QLatin1String("interpolation") +#define KEY_INVERSE_BIND_MATRICES QLatin1String("inverseBindMatrices") +#define KEY_MATRIX QLatin1String("matrix") +#define KEY_MESH QLatin1String("mesh") +#define KEY_NAME QLatin1String("name") +#define KEY_NODE QLatin1String("node") +#define KEY_NODES QLatin1String("nodes") +#define KEY_OUTPUT QLatin1String("output") +#define KEY_PATH QLatin1String("path") +#define KEY_ROTATION QLatin1String("rotation") +#define KEY_SAMPLER QLatin1String("sampler") +#define KEY_SAMPLERS QLatin1String("samplers") +#define KEY_SCALE QLatin1String("scale") +#define KEY_SKIN QLatin1String("skin") +#define KEY_SKINS QLatin1String("skins") +#define KEY_TARGET QLatin1String("target") +#define KEY_TRANSLATION QLatin1String("translation") +#define KEY_TYPE QLatin1String("type") +#define KEY_URI QLatin1String("uri") +#define KEY_VERSION QLatin1String("version") + +GLTFImporter::BufferData::BufferData() + : byteLength(0) + , data() +{ +} + +GLTFImporter::BufferData::BufferData(const QJsonObject &json) + : byteLength(json.value(KEY_BYTE_LENGTH).toInt()) + , path(json.value(KEY_URI).toString()) + , data() +{ +} + +GLTFImporter::BufferView::BufferView() + : byteOffset(0) + , byteLength(0) + , bufferIndex(-1) + , target(0) +{ +} + +GLTFImporter::BufferView::BufferView(const QJsonObject &json) + : byteOffset(json.value(KEY_BYTE_OFFSET).toInt()) + , byteLength(json.value(KEY_BYTE_LENGTH).toInt()) + , bufferIndex(json.value(KEY_BUFFER).toInt()) + , target(0) +{ + const auto targetValue = json.value(KEY_TARGET); + if (!targetValue.isUndefined()) + target = targetValue.toInt(); +} + +GLTFImporter::AccessorData::AccessorData() + : type(Qt3DRender::QAttribute::Float) + , dataSize(0) + , count(0) + , byteOffset(0) + , byteStride(0) +{ +} + +GLTFImporter::AccessorData::AccessorData(const QJsonObject &json) + : bufferViewIndex(json.value(KEY_BUFFER_VIEW).toInt(-1)) + , type(accessorTypeFromJSON(json.value(KEY_COMPONENT_TYPE).toInt())) + , dataSize(accessorDataSizeFromJson(json.value(KEY_TYPE).toString())) + , count(json.value(KEY_COUNT).toInt()) + , byteOffset(0) + , byteStride(0) +{ + const auto byteOffsetValue = json.value(KEY_BYTE_OFFSET); + if (!byteOffsetValue.isUndefined()) + byteOffset = byteOffsetValue.toInt(); + const auto byteStrideValue = json.value(KEY_BYTE_STRIDE); + if (!byteStrideValue.isUndefined()) + byteStride = byteStrideValue.toInt(); +} + +GLTFImporter::Skin::Skin() + : inverseBindAccessorIndex(-1) + , jointNodeIndices() +{ +} + +GLTFImporter::Skin::Skin(const QJsonObject &json) + : name(json.value(KEY_NAME).toString()) + , inverseBindAccessorIndex(json.value(KEY_INVERSE_BIND_MATRICES).toInt()) +{ + QJsonArray jointNodes = json.value(KEY_JOINTS).toArray(); + jointNodeIndices.reserve(jointNodes.size()); + for (const auto jointNodeValue : jointNodes) + jointNodeIndices.push_back(jointNodeValue.toInt()); +} + +GLTFImporter::Channel::Channel() + : samplerIndex(-1) + , targetNodeIndex(-1) + , targetProperty() +{ +} + +GLTFImporter::Channel::Channel(const QJsonObject &json) + : samplerIndex(json.value(KEY_SAMPLER).toInt()) + , targetNodeIndex(-1) + , targetProperty() +{ + const auto targetJson = json.value(KEY_TARGET).toObject(); + targetNodeIndex = targetJson.value(KEY_NODE).toInt(); + targetProperty = targetJson.value(KEY_PATH).toString(); +} + +GLTFImporter::Sampler::Sampler() + : inputAccessorIndex(-1) + , outputAccessorIndex(-1) + , interpolationMode(Linear) +{ +} + +GLTFImporter::Sampler::Sampler(const QJsonObject &json) + : inputAccessorIndex(json.value(KEY_INPUT).toInt()) + , outputAccessorIndex(json.value(KEY_OUTPUT).toInt()) + , interpolationMode(Linear) +{ + const auto interpolation = json.value(KEY_INTERPOLATION).toString(); + if (interpolation == QLatin1String("LINEAR")) + interpolationMode = Linear; + else if (interpolation == QLatin1String("STEP")) + interpolationMode = Step; + else if (interpolation == QLatin1String("CATMULLROMSPLINE")) + interpolationMode = CatmullRomSpline; + else if (interpolation == QLatin1String("CUBICSPLINE")) + interpolationMode = CubicSpline; +} + +QString GLTFImporter::Sampler::interpolationModeString() const +{ + switch (interpolationMode) { + case Linear: return QLatin1String("LINEAR"); + case Step: return QLatin1String("STEP"); + case CatmullRomSpline: return QLatin1String("CATMULLROMSPLINE"); + case CubicSpline: return QLatin1String("CUBICSPLINE"); + } + + return QLatin1String("Unknown"); +} + +GLTFImporter::Animation::Animation() + : name() + , channels() + , samplers() +{ +} + +GLTFImporter::Animation::Animation(const QJsonObject &json) + : name(json.value(KEY_NAME).toString()) +{ + QJsonArray channelsArray = json.value(KEY_CHANNELS).toArray(); + channels.reserve(channelsArray.size()); + for (const auto channelValue : channelsArray) { + Channel channel(channelValue.toObject()); + channels.push_back(channel); + } + + QJsonArray samplersArray = json.value(KEY_SAMPLERS).toArray(); + samplers.reserve(samplersArray.size()); + for (const auto samplerValue : samplersArray) { + Sampler sampler(samplerValue.toObject()); + samplers.push_back(sampler); + } +} + +GLTFImporter::Node::Node() + : localTransform() + , childNodeIndices() + , name() + , parentNodeIndex(-1) + , cameraIndex(-1) + , meshIndex(-1) + , skinIndex(-1) +{ +} + +GLTFImporter::Node::Node(const QJsonObject &json) + : localTransform() + , childNodeIndices() + , name(json.value(KEY_NAME).toString()) + , parentNodeIndex(-1) + , cameraIndex(-1) + , meshIndex(-1) + , skinIndex(-1) +{ + // Child nodes - we setup the parent links in a later pass + QJsonArray childNodes = json.value(KEY_CHILDREN).toArray(); + childNodeIndices.reserve(childNodes.size()); + for (const auto childNodeValue : childNodes) + childNodeIndices.push_back(childNodeValue.toInt()); + + // Local transform - matrix or scale, rotation, translation + const auto matrixValue = json.value(KEY_MATRIX); + if (!matrixValue.isUndefined()) { + jsonArrayToSqt(matrixValue.toArray(), localTransform); + } else { + const auto scaleValue = json.value(KEY_SCALE); + const auto rotationValue = json.value(KEY_ROTATION); + const auto translationValue = json.value(KEY_TRANSLATION); + + QVector3D s(1.0f, 1.0f, 1.0f); + if (!scaleValue.isUndefined()) + jsonArrayToVector3D(scaleValue.toArray(), localTransform.scale); + + QQuaternion r; + if (!rotationValue.isUndefined()) + jsonArrayToQuaternion(json.value(KEY_ROTATION).toArray(), localTransform.rotation); + + QVector3D t; + if (!translationValue.isUndefined()) + jsonArrayToVector3D(json.value(KEY_TRANSLATION).toArray(), localTransform.translation); + } + + // Referenced objects + const auto cameraValue = json.value(KEY_CAMERA); + if (!cameraValue.isUndefined()) + cameraIndex = cameraValue.toInt(); + + const auto meshValue = json.value(KEY_MESH); + if (!meshValue.isUndefined()) + meshIndex = meshValue.toInt(); + + const auto skinValue = json.value(KEY_SKIN); + if (!skinValue.isUndefined()) + skinIndex = skinValue.toInt(); +} + +Qt3DRender::QAttribute::VertexBaseType GLTFImporter::accessorTypeFromJSON(int componentType) +{ + if (componentType == GL_BYTE) + return Qt3DRender::QAttribute::Byte; + else if (componentType == GL_UNSIGNED_BYTE) + return Qt3DRender::QAttribute::UnsignedByte; + else if (componentType == GL_SHORT) + return Qt3DRender::QAttribute::Short; + else if (componentType == GL_UNSIGNED_SHORT) + return Qt3DRender::QAttribute::UnsignedShort; + else if (componentType == GL_UNSIGNED_INT) + return Qt3DRender::QAttribute::UnsignedInt; + else if (componentType == GL_FLOAT) + return Qt3DRender::QAttribute::Float; + + // There shouldn't be an invalid case here + qWarning("unsupported accessor type %d", componentType); + return Qt3DRender::QAttribute::Float; +} + +uint GLTFImporter::accessorTypeSize(Qt3DRender::QAttribute::VertexBaseType componentType) +{ + switch (componentType) { + case Qt3DRender::QAttribute::Byte: + case Qt3DRender::QAttribute::UnsignedByte: + return 1; + + case Qt3DRender::QAttribute::Short: + case Qt3DRender::QAttribute::UnsignedShort: + return 2; + + case Qt3DRender::QAttribute::Int: + case Qt3DRender::QAttribute::Float: + return 4; + + default: + qWarning("Unhandled accessor data type %d", componentType); + return 0; + } +} + +uint GLTFImporter::accessorDataSizeFromJson(const QString &type) +{ + QString typeName = type.toUpper(); + if (typeName == QLatin1String("SCALAR")) + return 1; + if (typeName == QLatin1String("VEC2")) + return 2; + if (typeName == QLatin1String("VEC3")) + return 3; + if (typeName == QLatin1String("VEC4")) + return 4; + if (typeName == QLatin1String("MAT2")) + return 4; + if (typeName == QLatin1String("MAT3")) + return 9; + if (typeName == QLatin1String("MAT4")) + return 16; + + return 0; +} + +GLTFImporter::GLTFImporter() +{ +} + +bool GLTFImporter::load(QIODevice *ioDev) +{ + QByteArray jsonData = ioDev->readAll(); + QJsonDocument sceneDocument = QJsonDocument::fromBinaryData(jsonData); + if (sceneDocument.isNull()) + sceneDocument = QJsonDocument::fromJson(jsonData); + + if (Q_UNLIKELY(!setJSON(sceneDocument))) { + qWarning("not a JSON document"); + return false; + } + + auto file = qobject_cast<QFile*>(ioDev); + if (file) { + QFileInfo finfo(file->fileName()); + setBasePath(finfo.dir().absolutePath()); + } + + return parse(); +} + +QHash<int, int> GLTFImporter::createNodeIndexToJointIndexMap(const Skin &skin) const +{ + const int jointCount = skin.jointNodeIndices.size(); + QHash<int, int> nodeIndexToJointIndexMap; + nodeIndexToJointIndexMap.reserve(jointCount); + for (int i = 0; i < jointCount; ++i) + nodeIndexToJointIndexMap.insert(skin.jointNodeIndices[i], i); + return nodeIndexToJointIndexMap; +} + +QVector<Qt3DAnimation::Animation::Channel> GLTFImporter::createAnimationData(const QString &animationName) const +{ + QVector<Qt3DAnimation::Animation::Channel> channels; + if (m_animations.isEmpty()) { + qCWarning(Jobs) << "File does not contain any animation data"; + return channels; + } + + int animationIndex = 0; + if (!animationName.isEmpty()) { + for (int i = 0; i < m_animations.size(); ++i) { + if (m_animations[i].name == animationName) { + animationIndex = i; + break; + } + } + } + const Animation &animation = m_animations[animationIndex]; + + // Create node index to joint index lookup tables for each skin + QVector<QHash<int, int>> nodeIndexToJointIndexMaps; + nodeIndexToJointIndexMaps.reserve(m_skins.size()); + for (const auto &skin : m_skins) + nodeIndexToJointIndexMaps.push_back(createNodeIndexToJointIndexMap(skin)); + + int channelIndex = 0; + for (const auto &channel : animation.channels) { + Qt3DAnimation::Animation::Channel outputChannel; + outputChannel.name = gltfTargetPropertyToChannelName(channel.targetProperty); + + // Find the node index to joint index map that contains the target node and + // look up the joint index from it. If no such map is found, the target joint + // is not part of a skeleton and so we can just set the jointIndex to -1. + int jointIndex = -1; + for (const auto &map : nodeIndexToJointIndexMaps) { + const auto result = map.find(channel.targetNodeIndex); + if (result != map.cend()) { + jointIndex = result.value(); + break; + } + } + outputChannel.jointIndex = jointIndex; + + const auto &sampler = animation.samplers[channel.samplerIndex]; + const auto interpolationType = gltfToQKeyFrameInterpolation(sampler.interpolationMode); + + if (sampler.inputAccessorIndex == -1 || sampler.outputAccessorIndex == -1) { + qWarning() << "Skipping channel due to invalid accessor indices in the sampler" << endl; + continue; + } + + const auto &inputAccessor = m_accessors[sampler.inputAccessorIndex]; + const auto &outputAccessor = m_accessors[sampler.outputAccessorIndex]; + + if (inputAccessor.type != Qt3DRender::QAttribute::Float) { + qWarning() << "Input accessor has wrong data type. Skipping channel."; + continue; + } + + if (outputAccessor.type != Qt3DRender::QAttribute::Float) { + qWarning() << "Output accessor has wrong data type. Skipping channel."; + continue; + } + + if (inputAccessor.count != outputAccessor.count) { + qWarning() << "Warning!!! Input accessor has" << inputAccessor.count + << "entries and output accessor has" << outputAccessor.count + << "entries. They should match. Please check your data."; + continue; + } + + // TODO: Allow Qt 3D animation data to share timestamps between multiple + // channel components. I.e. allow key frame values of composite types. + // Doesn't give as much freedom but more efficient at runtime. + + // Get the key frame times first as these are common to all components of the + // key frame values. + const int keyFrameCount = inputAccessor.count; + QVector<float> keyframeTimes(keyFrameCount); + for (int i = 0; i < keyFrameCount; ++i) { + const auto rawTimestamp = accessorData(sampler.inputAccessorIndex, i); + keyframeTimes[i] = *reinterpret_cast<const float*>(rawTimestamp.data); + } + + // Create a ChannelComponent for each component of the output sampler and + // populate it with data. + switch (outputAccessor.dataSize) { + // TODO: Handle other types as needed + case 3: { + // vec3 + const int componentCount = 3; + + // Construct the channel component names and add component to the channel + const QStringList suffixes + = (QStringList() << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z")); + outputChannel.channelComponents.resize(componentCount); + for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { + outputChannel.channelComponents[componentIndex].name + = QString(QLatin1String("%1 %2")).arg(outputChannel.name, + suffixes[componentIndex]); + } + + // Populate the fcurves in the channel components + for (int i = 0; i < keyFrameCount; ++i) { + const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i); + QVector3D v; + memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength); + + for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { + Keyframe keyFrame; + keyFrame.interpolation = interpolationType; + keyFrame.value = v[componentIndex]; + outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame); + } + } + + break; + } // case 3 + + case 4: { + // vec4 or quaternion + const int componentCount = 4; + + // Construct the channel component names and add component to the channel + const QStringList rotationSuffixes = (QStringList() + << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z") << QLatin1String("W")); + const QStringList standardSuffixes = (QStringList() + << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z")); + const QStringList suffixes = (channel.targetProperty == QLatin1String("rotation")) + ? rotationSuffixes : standardSuffixes; + outputChannel.channelComponents.resize(componentCount); + for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { + outputChannel.channelComponents[componentIndex].name + = QString(QLatin1String("%1 %2")).arg(outputChannel.name, + suffixes[componentIndex]); + } + + // Populate the fcurves in the channel components + for (int i = 0; i < keyFrameCount; ++i) { + const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i); + QVector4D v; + memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength); + + for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) { + Keyframe keyFrame; + keyFrame.interpolation = interpolationType; + keyFrame.value = v[componentIndex]; + outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame); + } + } + + break; + } // case 4 + } + + channels.push_back(outputChannel); + ++channelIndex; + } + + return channels; +} + +GLTFImporter::RawData GLTFImporter::accessorData(int accessorIndex, int index) const +{ + const AccessorData &accessor = m_accessors[accessorIndex]; + const BufferView &bufferView = m_bufferViews[accessor.bufferViewIndex]; + const BufferData &bufferData = m_bufferDatas[bufferView.bufferIndex]; + const QByteArray &ba = bufferData.data; + const char *rawData = ba.constData() + bufferView.byteOffset + accessor.byteOffset; + + const uint typeSize = accessorTypeSize(accessor.type); + const int stride = (accessor.byteStride == 0) + ? accessor.dataSize * typeSize + : accessor.byteStride; + + const char* data = rawData + index * stride; + if (data - rawData > ba.size()) { + qWarning("Attempting to access data beyond end of buffer"); + return RawData{ nullptr, 0 }; + } + + const quint64 byteLength = accessor.dataSize * typeSize; + RawData rd{ data, byteLength }; + + return rd; +} + +void GLTFImporter::setBasePath(const QString &path) +{ + m_basePath = path; +} + +bool GLTFImporter::setJSON(const QJsonDocument &json) +{ + if (!json.isObject()) + return false; + m_json = json; + cleanup(); + return true; +} + +bool GLTFImporter::parse() +{ + // Find the glTF version + const QJsonObject asset = m_json.object().value(KEY_ASSET).toObject(); + const QString versionString = asset.value(KEY_VERSION).toString(); + const auto version = QVersionNumber::fromString(versionString); + switch (version.majorVersion()) { + case 2: + return parseGLTF2(); + + default: + qWarning() << "Unsupported version of glTF" << versionString; + return false; + } +} + +bool GLTFImporter::parseGLTF2() +{ + bool success = true; + const QJsonArray buffers = m_json.object().value(KEY_BUFFERS).toArray(); + for (const auto &bufferValue : buffers) + success &= processJSONBuffer(bufferValue.toObject()); + + const QJsonArray bufferViews = m_json.object().value(KEY_BUFFER_VIEWS).toArray(); + for (const auto &bufferViewValue : bufferViews) + success &= processJSONBufferView(bufferViewValue.toObject()); + + const QJsonArray accessors = m_json.object().value(KEY_ACCESSORS).toArray(); + for (const auto &accessorValue : accessors) + success &= processJSONAccessor(accessorValue.toObject()); + + const QJsonArray skins = m_json.object().value(KEY_SKINS).toArray(); + for (const auto &skinValue : skins) + success &= processJSONSkin(skinValue.toObject()); + + const QJsonArray animations = m_json.object().value(KEY_ANIMATIONS).toArray(); + for (const auto &animationValue : animations) + success &= processJSONAnimation(animationValue.toObject()); + + const QJsonArray nodes = m_json.object().value(KEY_NODES).toArray(); + for (const auto &nodeValue : nodes) + success &= processJSONNode(nodeValue.toObject()); + setupNodeParentLinks(); + + // TODO: Make a complete GLTF 2 parser by extending to other top level elements: + // scenes, animations, meshes etc. + + return success; +} + +void GLTFImporter::cleanup() +{ + m_accessors.clear(); + m_bufferViews.clear(); + m_bufferDatas.clear(); +} + +bool GLTFImporter::processJSONBuffer(const QJsonObject &json) +{ + // Store buffer details and load data into memory + BufferData buffer(json); + buffer.data = resolveLocalData(buffer.path); + if (buffer.data.isEmpty()) + return false; + + m_bufferDatas.push_back(buffer); + return true; +} + +bool GLTFImporter::processJSONBufferView(const QJsonObject &json) +{ + BufferView bufferView(json); + + // Perform sanity checks + const auto bufferIndex = bufferView.bufferIndex; + if (Q_UNLIKELY(bufferIndex) >= m_bufferDatas.size()) { + qWarning("Unknown buffer %d when processing buffer view", bufferIndex); + return false; + } + + const auto &bufferData = m_bufferDatas[bufferIndex]; + if (bufferView.byteOffset > bufferData.byteLength) { + qWarning("Bufferview has offset greater than buffer %d length", bufferIndex); + return false; + } + + if (Q_UNLIKELY(bufferView.byteOffset + bufferView.byteLength > bufferData.byteLength)) { + qWarning("BufferView extends beyond end of buffer %d", bufferIndex); + return false; + } + + m_bufferViews.push_back(bufferView); + return true; +} + +bool GLTFImporter::processJSONAccessor(const QJsonObject &json) +{ + AccessorData accessor(json); + + // TODO: Perform sanity checks + + m_accessors.push_back(accessor); + return true; +} + +bool GLTFImporter::processJSONSkin(const QJsonObject &json) +{ + Skin skin(json); + + // TODO: Perform sanity checks + + m_skins.push_back(skin); + return true; +} + +bool GLTFImporter::processJSONAnimation(const QJsonObject &json) +{ + Animation animation(json); + + for (const auto &channel : animation.channels) { + if (channel.samplerIndex == -1) + qWarning() << "Invalid sampler index in animation" + << animation.name << "for channel targeting node" + << channel.targetNodeIndex << " and property" + << channel.targetProperty; + } + + for (const auto &sampler : animation.samplers) { + if (sampler.inputAccessorIndex == -1) { + qWarning() << "Sampler for animaton" << animation.name + << "references has an invalid input accessor index"; + } + + if (sampler.outputAccessorIndex == -1) { + qWarning() << "Sampler for animaton" << animation.name + << "references has an invalid output accessor index"; + } + } + + m_animations.push_back(animation); + return true; +} + +bool GLTFImporter::processJSONNode(const QJsonObject &json) +{ + Node node(json); + + // TODO: Perform sanity checks + + m_nodes.push_back(node); + return true; +} + +void GLTFImporter::setupNodeParentLinks() +{ + const int nodeCount = m_nodes.size(); + for (int i = 0; i < nodeCount; ++i) { + const Node &node = m_nodes[i]; + const QVector<int> &childNodeIndices = node.childNodeIndices; + for (const auto childNodeIndex : childNodeIndices) { + Q_ASSERT(childNodeIndex < m_nodes.size()); + Node &childNode = m_nodes[childNodeIndex]; + Q_ASSERT(childNode.parentNodeIndex == -1); + childNode.parentNodeIndex = i; + } + } +} + +QByteArray GLTFImporter::resolveLocalData(const QString &path) const +{ + QDir d(m_basePath); + Q_ASSERT(d.exists()); + + QString absPath = d.absoluteFilePath(path); + QFile f(absPath); + f.open(QIODevice::ReadOnly); + return f.readAll(); +} + +} // namespace Animation +} // namespace Qt3DAnimation + +QT_END_NAMESPACE diff --git a/src/animation/backend/gltfimporter_p.h b/src/animation/backend/gltfimporter_p.h new file mode 100644 index 000000000..ae7674fe7 --- /dev/null +++ b/src/animation/backend/gltfimporter_p.h @@ -0,0 +1,232 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt3D module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QT3DANIMATION_ANIMATION_GLTFIMPORTER_H +#define QT3DANIMATION_ANIMATION_GLTFIMPORTER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGlobal> +#include <Qt3DAnimation/private/fcurve_p.h> +#include <Qt3DRender/qattribute.h> +#include <Qt3DCore/private/sqt_p.h> +#include <Qt3DCore/private/qmath3d_p.h> + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <QVector> + +QT_BEGIN_NAMESPACE + +class QIODevice; + +namespace Qt3DAnimation { +namespace Animation { + +class GLTFImporter +{ +public: + class BufferData + { + public: + BufferData(); + explicit BufferData(const QJsonObject &json); + + quint64 byteLength; + QString path; + QByteArray data; + }; + + class BufferView + { + public: + BufferView(); + explicit BufferView(const QJsonObject &json); + + quint64 byteOffset; + quint64 byteLength; + int bufferIndex; + int target; // Only for per vertex attributes + }; + + class AccessorData + { + public: + AccessorData(); + explicit AccessorData(const QJsonObject &json); + + int bufferViewIndex; + Qt3DRender::QAttribute::VertexBaseType type; + uint dataSize; + int count; + int byteOffset; + int byteStride; // Only for per vertex attributes + + // TODO: Extend to support sparse accessors + }; + + class Skin + { + public: + Skin(); + explicit Skin(const QJsonObject &json); + + QString name; + int inverseBindAccessorIndex; + QVector<int> jointNodeIndices; + }; + + class Channel + { + public: + Channel(); + explicit Channel(const QJsonObject &json); + + int samplerIndex; + int targetNodeIndex; + QString targetProperty; + }; + + class Sampler + { + public: + Sampler(); + explicit Sampler(const QJsonObject &json); + + enum InterpolationMode { + Linear, + Step, + CatmullRomSpline, + CubicSpline + }; + + QString interpolationModeString() const; + + int inputAccessorIndex; + int outputAccessorIndex; + InterpolationMode interpolationMode; + }; + + class Animation + { + public: + Animation(); + explicit Animation(const QJsonObject &json); + + QString name; + QVector<Channel> channels; + QVector<Sampler> samplers; + }; + + class Node + { + public: + Node(); + explicit Node(const QJsonObject &json); + + Qt3DCore::Sqt localTransform; + QVector<int> childNodeIndices; + QString name; + int parentNodeIndex; + int cameraIndex; + int meshIndex; + int skinIndex; + }; + + GLTFImporter(); + + bool load(QIODevice *ioDev); + const QVector<Animation> animations() const { return m_animations; } + + QVector<Qt3DAnimation::Animation::Channel> createAnimationData(const QString &animationName = QString()) const; + +private: + static Qt3DRender::QAttribute::VertexBaseType accessorTypeFromJSON(int componentType); + static uint accessorTypeSize(Qt3DRender::QAttribute::VertexBaseType componentType); + static uint accessorDataSizeFromJson(const QString &type); + + struct RawData + { + const char *data; + quint64 byteLength; + }; + + void setBasePath(const QString &path); + bool setJSON(const QJsonDocument &json); + + bool parse(); + bool parseGLTF2(); + void cleanup(); + QHash<int, int> createNodeIndexToJointIndexMap(const Skin &skin) const; + + bool processJSONBuffer(const QJsonObject &json); + bool processJSONBufferView(const QJsonObject &json); + bool processJSONAccessor(const QJsonObject &json); + bool processJSONSkin(const QJsonObject &json); + bool processJSONAnimation(const QJsonObject &json); + bool processJSONNode(const QJsonObject &json); + void setupNodeParentLinks(); + QByteArray resolveLocalData(const QString &path) const; + + RawData accessorData(int accessorIndex, int index) const; + + QJsonDocument m_json; + QString m_basePath; + QVector<BufferData> m_bufferDatas; + QVector<BufferView> m_bufferViews; + QVector<AccessorData> m_accessors; + QVector<Skin> m_skins; + QVector<Animation> m_animations; + QVector<Node> m_nodes; +}; + +} // namespace Animation +} // namespace Qt3DAnimation + +QT_END_NAMESPACE + +#endif // QT3DANIMATION_ANIMATION_GLTFIMPORTER_H |