mirror of https://github.com/qt/qt3d.git
376 lines
12 KiB
C++
376 lines
12 KiB
C++
/****************************************************************************
|
|
**
|
|
** 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 <Qt3DCore/qpropertyupdatedchange.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)
|
|
{
|
|
}
|
|
|
|
void AnimationClip::initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change)
|
|
{
|
|
const auto loaderTypedChange = qSharedPointerDynamicCast<Qt3DCore::QNodeCreatedChange<QAnimationClipLoaderData>>(change);
|
|
if (loaderTypedChange) {
|
|
const auto &data = loaderTypedChange->data;
|
|
m_dataType = File;
|
|
m_source = data.source;
|
|
if (!m_source.isEmpty())
|
|
setDirty(Handler::AnimationClipDirty);
|
|
return;
|
|
}
|
|
|
|
const auto clipTypedChange = qSharedPointerDynamicCast<Qt3DCore::QNodeCreatedChange<QAnimationClipChangeData>>(change);
|
|
if (clipTypedChange) {
|
|
const auto &data = clipTypedChange->data;
|
|
m_dataType = Data;
|
|
m_clipData = data.clipData;
|
|
if (m_clipData.isValid())
|
|
setDirty(Handler::AnimationClipDirty);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
clearData();
|
|
}
|
|
|
|
void AnimationClip::setStatus(QAnimationClipLoader::Status 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 AnimationClip::sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e)
|
|
{
|
|
switch (e->type()) {
|
|
case Qt3DCore::PropertyUpdated: {
|
|
const auto change = qSharedPointerCast<Qt3DCore::QPropertyUpdatedChange>(e);
|
|
if (change->propertyName() == QByteArrayLiteral("source")) {
|
|
Q_ASSERT(m_dataType == File);
|
|
m_source = change->value().toUrl();
|
|
setDirty(Handler::AnimationClipDirty);
|
|
} else if (change->propertyName() == QByteArrayLiteral("clipData")) {
|
|
Q_ASSERT(m_dataType == Data);
|
|
m_clipData = change->value().value<Qt3DAnimation::QAnimationClipData>();
|
|
if (m_clipData.isValid())
|
|
setDirty(Handler::AnimationClipDirty);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
QBackendNode::sceneChangeEvent(e);
|
|
}
|
|
|
|
/*!
|
|
\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
|
|
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);
|
|
}
|
|
} 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;
|
|
|
|
// Send a change to the frontend
|
|
auto e = Qt3DCore::QPropertyUpdatedChangePtr::create(peerId());
|
|
e->setDeliveryFlags(Qt3DCore::QSceneChange::DeliverToAll);
|
|
e->setPropertyName("duration");
|
|
e->setValue(m_duration);
|
|
notifyObservers(e);
|
|
}
|
|
|
|
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
|