1/****************************************************************************
2**
3** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
4** Contact: http://www.qt-project.org/legal
5**
6** This file is part of the Qt3D module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL3$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or later as published by the Free
28** Software Foundation and appearing in the file LICENSE.GPL included in
29** the packaging of this file. Please review the following information to
30** ensure the GNU General Public License version 2.0 requirements will be
31** met: http://www.gnu.org/licenses/gpl-2.0.html.
32**
33** $QT_END_LICENSE$
34**
35****************************************************************************/
36
37#include "animationclip_p.h"
38#include <Qt3DAnimation/qanimationclip.h>
39#include <Qt3DAnimation/qanimationcliploader.h>
40#include <Qt3DAnimation/private/qanimationclip_p.h>
41#include <Qt3DAnimation/private/qanimationcliploader_p.h>
42#include <Qt3DAnimation/private/animationlogging_p.h>
43#include <Qt3DAnimation/private/managers_p.h>
44#include <Qt3DAnimation/private/gltfimporter_p.h>
45#include <Qt3DRender/private/qurlhelper_p.h>
46
47#include <QtCore/qbytearray.h>
48#include <QtCore/qfile.h>
49#include <QtCore/qjsonarray.h>
50#include <QtCore/qjsondocument.h>
51#include <QtCore/qjsonobject.h>
52#include <QtCore/qurlquery.h>
53
54QT_BEGIN_NAMESPACE
55
56#define ANIMATION_INDEX_KEY QLatin1String("animationIndex")
57#define ANIMATION_NAME_KEY QLatin1String("animationName")
58
59namespace Qt3DAnimation {
60namespace Animation {
61
62AnimationClip::AnimationClip()
63 : BackendNode(ReadWrite)
64 , m_source()
65 , m_status(QAnimationClipLoader::NotReady)
66 , m_clipData()
67 , m_dataType(Unknown)
68 , m_name()
69 , m_channels()
70 , m_duration(0.0f)
71 , m_channelComponentCount(0)
72{
73}
74
75void AnimationClip::cleanup()
76{
77 setEnabled(false);
78 m_handler = nullptr;
79 m_source.clear();
80 m_clipData.clearChannels();
81 m_status = QAnimationClipLoader::NotReady;
82 m_dataType = Unknown;
83 m_channels.clear();
84 m_duration = 0.0f;
85 m_channelComponentCount = 0;
86
87 clearData();
88}
89
90void AnimationClip::setStatus(QAnimationClipLoader::Status status)
91{
92 if (status != m_status) {
93 m_status = status;
94 }
95}
96
97void AnimationClip::syncFromFrontEnd(const Qt3DCore::QNode *frontEnd, bool firstTime)
98{
99 BackendNode::syncFromFrontEnd(frontEnd, firstTime);
100 const QAbstractAnimationClip *node = qobject_cast<const QAbstractAnimationClip *>(object: frontEnd);
101 if (!node)
102 return;
103
104 const QAnimationClip *clipNode = qobject_cast<const QAnimationClip *>(object: frontEnd);
105 if (clipNode) {
106 if (firstTime)
107 m_dataType = Data;
108 Q_ASSERT(m_dataType == Data);
109 if (m_clipData != clipNode->clipData()) {
110 m_clipData = clipNode->clipData();
111 if (m_clipData.isValid())
112 setDirty(Handler::AnimationClipDirty);
113 }
114 }
115
116 const QAnimationClipLoader *loaderNode = qobject_cast<const QAnimationClipLoader *>(object: frontEnd);
117 if (loaderNode) {
118 if (firstTime)
119 m_dataType = File;
120 Q_ASSERT(m_dataType == File);
121 if (m_source != loaderNode->source()) {
122 m_source = loaderNode->source();
123 if (!m_source.isEmpty())
124 setDirty(Handler::AnimationClipDirty);
125 }
126 }
127}
128
129/*!
130 \internal
131 Called by LoadAnimationClipJob on the threadpool
132 */
133void AnimationClip::loadAnimation()
134{
135 qCDebug(Jobs) << Q_FUNC_INFO << m_source;
136 clearData();
137
138 // Load the data
139 switch (m_dataType) {
140 case File:
141 loadAnimationFromUrl();
142 break;
143
144 case Data:
145 loadAnimationFromData();
146 break;
147
148 default:
149 Q_UNREACHABLE();
150 }
151
152 // Update the duration
153 const float t = findDuration();
154 setDuration(t);
155
156 m_channelComponentCount = findChannelComponentCount();
157
158 // If using a loader inform the frontend of the status change
159 if (m_source.isEmpty()) {
160 if (qFuzzyIsNull(f: t) || m_channelComponentCount == 0)
161 setStatus(QAnimationClipLoader::Error);
162 else
163 setStatus(QAnimationClipLoader::Ready);
164 }
165
166 // notify all ClipAnimators and BlendedClipAnimators that depend on this clip,
167 // that the clip has changed and that they are now dirty
168 {
169 QMutexLocker lock(&m_mutex);
170 for (const Qt3DCore::QNodeId id : qAsConst(t&: m_dependingAnimators)) {
171 ClipAnimator *animator = m_handler->clipAnimatorManager()->lookupResource(id);
172 if (animator)
173 animator->animationClipMarkedDirty();
174 }
175 for (const Qt3DCore::QNodeId id : qAsConst(t&: m_dependingBlendedAnimators)) {
176 BlendedClipAnimator *animator = m_handler->blendedClipAnimatorManager()->lookupResource(id);
177 if (animator)
178 animator->animationClipMarkedDirty();
179 }
180 m_dependingAnimators.clear();
181 m_dependingBlendedAnimators.clear();
182 }
183
184 qCDebug(Jobs) << "Loaded animation data:" << *this;
185}
186
187void AnimationClip::loadAnimationFromUrl()
188{
189 // TODO: Handle remote files
190 QString filePath = Qt3DRender::QUrlHelper::urlToLocalFileOrQrc(url: m_source);
191 QFile file(filePath);
192 if (!file.open(flags: QIODevice::ReadOnly)) {
193 qWarning() << "Could not find animation clip:" << filePath;
194 setStatus(QAnimationClipLoader::Error);
195 return;
196 }
197
198 // Extract the animationName or animationIndex from the url query parameters.
199 // If both present, animationIndex wins.
200 int animationIndex = -1;
201 QString animationName;
202 if (m_source.hasQuery()) {
203 QUrlQuery query(m_source);
204 if (query.hasQueryItem(ANIMATION_INDEX_KEY)) {
205 bool ok = false;
206 int i = query.queryItemValue(ANIMATION_INDEX_KEY).toInt(ok: &ok);
207 if (ok)
208 animationIndex = i;
209 }
210
211 if (animationIndex == -1 && query.hasQueryItem(ANIMATION_NAME_KEY)) {
212 animationName = query.queryItemValue(ANIMATION_NAME_KEY);
213 }
214
215 qCDebug(Jobs) << "animationIndex =" << animationIndex;
216 qCDebug(Jobs) << "animationName =" << animationName;
217 }
218
219 // TODO: Convert to plugins
220 // Load glTF or "native"
221 if (filePath.endsWith(s: QLatin1String("gltf"))) {
222 qCDebug(Jobs) << "Loading glTF animation from" << filePath;
223 GLTFImporter gltf;
224 gltf.load(ioDev: &file);
225 auto nameAndChannels = gltf.createAnimationData(animationIndex, animationName);
226 m_name = nameAndChannels.name;
227 m_channels = nameAndChannels.channels;
228 } else if (filePath.endsWith(s: QLatin1String("json"))) {
229 // Native format
230 QByteArray animationData = file.readAll();
231 QJsonDocument document = QJsonDocument::fromJson(json: animationData);
232 QJsonObject rootObject = document.object();
233
234 // TODO: Allow loading of a named animation from a file containing many
235 const QJsonArray animationsArray = rootObject[QLatin1String("animations")].toArray();
236 qCDebug(Jobs) << "Found" << animationsArray.size() << "animations:";
237 for (int i = 0; i < animationsArray.size(); ++i) {
238 QJsonObject animation = animationsArray.at(i).toObject();
239 qCDebug(Jobs) << "Animation Name:" << animation[QLatin1String("animationName")].toString();
240 }
241
242 // Find which animation clip to load from the file.
243 // Give priority to animationIndex over animationName
244 if (animationIndex >= animationsArray.size()) {
245 qCWarning(Jobs) << "Invalid animation index. Skipping.";
246 return;
247 }
248
249 if (animationsArray.size() == 1) {
250 animationIndex = 0;
251 } else if (animationIndex < 0 && !animationName.isEmpty()) {
252 // Can we find an animation of the correct name?
253 bool foundAnimation = false;
254 for (int i = 0; i < animationsArray.size(); ++i) {
255 if (animationsArray.at(i)[ANIMATION_NAME_KEY].toString() == animationName) {
256 animationIndex = i;
257 foundAnimation = true;
258 break;
259 }
260 }
261
262 if (!foundAnimation) {
263 qCWarning(Jobs) << "Invalid animation name. Skipping.";
264 return;
265 }
266 }
267
268 if (animationIndex < 0 || animationIndex >= animationsArray.size()) {
269 qCWarning(Jobs) << "Failed to find animation. Skipping.";
270 return;
271 }
272
273 QJsonObject animation = animationsArray.at(i: animationIndex).toObject();
274 m_name = animation[QLatin1String("animationName")].toString();
275
276 QJsonArray channelsArray = animation[QLatin1String("channels")].toArray();
277 const int channelCount = channelsArray.size();
278 m_channels.resize(asize: channelCount);
279 for (int i = 0; i < channelCount; ++i) {
280 const QJsonObject group = channelsArray.at(i).toObject();
281 m_channels[i].read(json: group);
282 }
283 } else {
284 qWarning() << "Unknown animation clip type. Please use json or glTF 2.0";
285 setStatus(QAnimationClipLoader::Error);
286 }
287}
288
289void AnimationClip::loadAnimationFromData()
290{
291 // Reformat data from QAnimationClipData to backend format
292 m_channels.resize(asize: m_clipData.channelCount());
293 int i = 0;
294 for (const auto &frontendChannel : qAsConst(t&: m_clipData))
295 m_channels[i++].setFromQChannel(frontendChannel);
296}
297
298void AnimationClip::addDependingClipAnimator(const Qt3DCore::QNodeId &id)
299{
300 QMutexLocker lock(&m_mutex);
301 m_dependingAnimators.push_back(t: id);
302}
303
304void AnimationClip::addDependingBlendedClipAnimator(const Qt3DCore::QNodeId &id)
305{
306 QMutexLocker lock(&m_mutex);
307 m_dependingBlendedAnimators.push_back(t: id);
308}
309
310void AnimationClip::setDuration(float duration)
311{
312 if (qFuzzyCompare(p1: duration, p2: m_duration))
313 return;
314
315 m_duration = duration;
316}
317
318int AnimationClip::channelIndex(const QString &channelName, int jointIndex) const
319{
320 const int channelCount = m_channels.size();
321 for (int i = 0; i < channelCount; ++i) {
322 if (m_channels[i].name == channelName
323 && (jointIndex == -1 || m_channels[i].jointIndex == jointIndex)) {
324 return i;
325 }
326 }
327 return -1;
328}
329
330/*!
331 \internal
332
333 Given the index of a channel, \a channelIndex, calculates
334 the base index of the first channelComponent in this group. For example, if
335 there are two channel groups each with 3 channels and you request
336 the channelBaseIndex(1), the return value will be 3. Indices 0-2 are
337 for the first group, so the first channel of the second group occurs
338 at index 3.
339 */
340int AnimationClip::channelComponentBaseIndex(int channelIndex) const
341{
342 int index = 0;
343 for (int i = 0; i < channelIndex; ++i)
344 index += m_channels[i].channelComponents.size();
345 return index;
346}
347
348void AnimationClip::clearData()
349{
350 m_name.clear();
351 m_channels.clear();
352}
353
354float AnimationClip::findDuration()
355{
356 // Iterate over the contained fcurves and find the longest one
357 float tMax = 0.f;
358 for (const Channel &channel : qAsConst(t&: m_channels)) {
359 for (const ChannelComponent &channelComponent : qAsConst(t: channel.channelComponents)) {
360 const float t = channelComponent.fcurve.endTime();
361 if (t > tMax)
362 tMax = t;
363 }
364 }
365 return tMax;
366}
367
368int AnimationClip::findChannelComponentCount()
369{
370 int channelCount = 0;
371 for (const Channel &channel : qAsConst(t&: m_channels))
372 channelCount += channel.channelComponents.size();
373 return channelCount;
374}
375
376} // namespace Animation
377} // namespace Qt3DAnimation
378
379QT_END_NAMESPACE
380

source code of qt3d/src/animation/backend/animationclip.cpp