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

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