| /**************************************************************************** |
| ** |
| ** 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 "animationclip_p.h" |
| #include <Qt3DAnimation/qanimationclip.h> |
| #include <Qt3DAnimation/qanimationcliploader.h> |
| #include <Qt3DAnimation/private/qanimationclip_p.h> |
| #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 <QtCore/qbytearray.h> |
| #include <QtCore/qfile.h> |
| #include <QtCore/qjsonarray.h> |
| #include <QtCore/qjsondocument.h> |
| #include <QtCore/qjsonobject.h> |
| #include <QtCore/qurlquery.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| #define ANIMATION_INDEX_KEY QLatin1String("animationIndex") |
| #define ANIMATION_NAME_KEY QLatin1String("animationName") |
| |
| namespace Qt3DAnimation { |
| namespace Animation { |
| |
| AnimationClip::AnimationClip() |
| : BackendNode(ReadWrite) |
| , m_source() |
| , m_status(QAnimationClipLoader::NotReady) |
| , m_clipData() |
| , m_dataType(Unknown) |
| , m_name() |
| , m_channels() |
| , m_duration(0.0f) |
| , m_channelComponentCount(0) |
| { |
| } |
| |
| void AnimationClip::cleanup() |
| { |
| setEnabled(false); |
| m_handler = nullptr; |
| m_source.clear(); |
| m_clipData.clearChannels(); |
| m_status = QAnimationClipLoader::NotReady; |
| m_dataType = Unknown; |
| m_channels.clear(); |
| m_duration = 0.0f; |
| m_channelComponentCount = 0; |
| |
| clearData(); |
| } |
| |
| void AnimationClip::setStatus(QAnimationClipLoader::Status status) |
| { |
| if (status != m_status) { |
| m_status = status; |
| } |
| } |
| |
| void AnimationClip::syncFromFrontEnd(const Qt3DCore::QNode *frontEnd, bool firstTime) |
| { |
| BackendNode::syncFromFrontEnd(frontEnd, firstTime); |
| const QAbstractAnimationClip *node = qobject_cast<const QAbstractAnimationClip *>(frontEnd); |
| if (!node) |
| return; |
| |
| const QAnimationClip *clipNode = qobject_cast<const QAnimationClip *>(frontEnd); |
| if (clipNode) { |
| if (firstTime) |
| m_dataType = Data; |
| Q_ASSERT(m_dataType == Data); |
| if (m_clipData != clipNode->clipData()) { |
| m_clipData = clipNode->clipData(); |
| if (m_clipData.isValid()) |
| setDirty(Handler::AnimationClipDirty); |
| } |
| } |
| |
| const QAnimationClipLoader *loaderNode = qobject_cast<const QAnimationClipLoader *>(frontEnd); |
| if (loaderNode) { |
| if (firstTime) |
| m_dataType = File; |
| Q_ASSERT(m_dataType == File); |
| if (m_source != loaderNode->source()) { |
| m_source = loaderNode->source(); |
| if (!m_source.isEmpty()) |
| setDirty(Handler::AnimationClipDirty); |
| } |
| } |
| } |
| |
| /*! |
| \internal |
| Called by LoadAnimationClipJob on the threadpool |
| */ |
| void AnimationClip::loadAnimation() |
| { |
| qCDebug(Jobs) << Q_FUNC_INFO << m_source; |
| clearData(); |
| |
| // Load the data |
| switch (m_dataType) { |
| case File: |
| loadAnimationFromUrl(); |
| break; |
| |
| case Data: |
| loadAnimationFromData(); |
| break; |
| |
| default: |
| Q_UNREACHABLE(); |
| } |
| |
| // Update the duration |
| const float t = findDuration(); |
| setDuration(t); |
| |
| m_channelComponentCount = findChannelComponentCount(); |
| |
| // If using a loader inform the frontend of the status change |
| if (m_source.isEmpty()) { |
| if (qFuzzyIsNull(t) || m_channelComponentCount == 0) |
| setStatus(QAnimationClipLoader::Error); |
| else |
| setStatus(QAnimationClipLoader::Ready); |
| } |
| |
| // notify all ClipAnimators and BlendedClipAnimators that depend on this clip, |
| // that the clip has changed and that they are now dirty |
| { |
| QMutexLocker lock(&m_mutex); |
| for (const Qt3DCore::QNodeId id : qAsConst(m_dependingAnimators)) { |
| ClipAnimator *animator = m_handler->clipAnimatorManager()->lookupResource(id); |
| if (animator) |
| animator->animationClipMarkedDirty(); |
| } |
| for (const Qt3DCore::QNodeId id : qAsConst(m_dependingBlendedAnimators)) { |
| BlendedClipAnimator *animator = m_handler->blendedClipAnimatorManager()->lookupResource(id); |
| if (animator) |
| animator->animationClipMarkedDirty(); |
| } |
| m_dependingAnimators.clear(); |
| m_dependingBlendedAnimators.clear(); |
| } |
| |
| qCDebug(Jobs) << "Loaded animation data:" << *this; |
| } |
| |
| void AnimationClip::loadAnimationFromUrl() |
| { |
| // TODO: Handle remote files |
| QString filePath = Qt3DRender::QUrlHelper::urlToLocalFileOrQrc(m_source); |
| QFile file(filePath); |
| if (!file.open(QIODevice::ReadOnly)) { |
| qWarning() << "Could not find animation clip:" << filePath; |
| setStatus(QAnimationClipLoader::Error); |
| return; |
| } |
| |
| // Extract the animationName or animationIndex from the url query parameters. |
| // If both present, animationIndex wins. |
| int animationIndex = -1; |
| QString animationName; |
| if (m_source.hasQuery()) { |
| QUrlQuery query(m_source); |
| if (query.hasQueryItem(ANIMATION_INDEX_KEY)) { |
| bool ok = false; |
| int i = query.queryItemValue(ANIMATION_INDEX_KEY).toInt(&ok); |
| if (ok) |
| animationIndex = i; |
| } |
| |
| if (animationIndex == -1 && query.hasQueryItem(ANIMATION_NAME_KEY)) { |
| animationName = query.queryItemValue(ANIMATION_NAME_KEY); |
| } |
| |
| qCDebug(Jobs) << "animationIndex =" << animationIndex; |
| qCDebug(Jobs) << "animationName =" << animationName; |
| } |
| |
| // 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); |
| auto nameAndChannels = gltf.createAnimationData(animationIndex, animationName); |
| m_name = nameAndChannels.name; |
| m_channels = nameAndChannels.channels; |
| } 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 |
| const 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(); |
| } |
| |
| // Find which animation clip to load from the file. |
| // Give priority to animationIndex over animationName |
| if (animationIndex >= animationsArray.size()) { |
| qCWarning(Jobs) << "Invalid animation index. Skipping."; |
| return; |
| } |
| |
| if (animationsArray.size() == 1) { |
| animationIndex = 0; |
| } else if (animationIndex < 0 && !animationName.isEmpty()) { |
| // Can we find an animation of the correct name? |
| bool foundAnimation = false; |
| for (int i = 0; i < animationsArray.size(); ++i) { |
| if (animationsArray.at(i)[ANIMATION_NAME_KEY].toString() == animationName) { |
| animationIndex = i; |
| foundAnimation = true; |
| break; |
| } |
| } |
| |
| if (!foundAnimation) { |
| qCWarning(Jobs) << "Invalid animation name. Skipping."; |
| return; |
| } |
| } |
| |
| if (animationIndex < 0 || animationIndex >= animationsArray.size()) { |
| qCWarning(Jobs) << "Failed to find animation. Skipping."; |
| return; |
| } |
| |
| QJsonObject animation = animationsArray.at(animationIndex).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); |
| } |
| } |
| |
| void AnimationClip::loadAnimationFromData() |
| { |
| // Reformat data from QAnimationClipData to backend format |
| m_channels.resize(m_clipData.channelCount()); |
| int i = 0; |
| for (const auto &frontendChannel : qAsConst(m_clipData)) |
| m_channels[i++].setFromQChannel(frontendChannel); |
| } |
| |
| void AnimationClip::addDependingClipAnimator(const Qt3DCore::QNodeId &id) |
| { |
| QMutexLocker lock(&m_mutex); |
| m_dependingAnimators.push_back(id); |
| } |
| |
| void AnimationClip::addDependingBlendedClipAnimator(const Qt3DCore::QNodeId &id) |
| { |
| QMutexLocker lock(&m_mutex); |
| m_dependingBlendedAnimators.push_back(id); |
| } |
| |
| void AnimationClip::setDuration(float duration) |
| { |
| if (qFuzzyCompare(duration, m_duration)) |
| return; |
| |
| m_duration = duration; |
| } |
| |
| int AnimationClip::channelIndex(const QString &channelName, int jointIndex) const |
| { |
| const int channelCount = m_channels.size(); |
| for (int i = 0; i < channelCount; ++i) { |
| if (m_channels[i].name == channelName |
| && (jointIndex == -1 || m_channels[i].jointIndex == jointIndex)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /*! |
| \internal |
| |
| Given the index of a channel, \a channelIndex, calculates |
| the base index of the first channelComponent in this group. For example, if |
| there are two channel groups each with 3 channels and you request |
| the channelBaseIndex(1), the return value will be 3. Indices 0-2 are |
| for the first group, so the first channel of the second group occurs |
| at index 3. |
| */ |
| int AnimationClip::channelComponentBaseIndex(int channelIndex) const |
| { |
| int index = 0; |
| for (int i = 0; i < channelIndex; ++i) |
| index += m_channels[i].channelComponents.size(); |
| return index; |
| } |
| |
| void AnimationClip::clearData() |
| { |
| m_name.clear(); |
| m_channels.clear(); |
| } |
| |
| float AnimationClip::findDuration() |
| { |
| // Iterate over the contained fcurves and find the longest one |
| double tMax = 0.0; |
| for (const Channel &channel : qAsConst(m_channels)) { |
| for (const ChannelComponent &channelComponent : qAsConst(channel.channelComponents)) { |
| const float t = channelComponent.fcurve.endTime(); |
| if (t > tMax) |
| tMax = t; |
| } |
| } |
| return tMax; |
| } |
| |
| int AnimationClip::findChannelComponentCount() |
| { |
| int channelCount = 0; |
| for (const Channel &channel : qAsConst(m_channels)) |
| channelCount += channel.channelComponents.size(); |
| return channelCount; |
| } |
| |
| } // namespace Animation |
| } // namespace Qt3DAnimation |
| |
| QT_END_NAMESPACE |