/**************************************************************************** ** ** 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 "animationutils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace Qt3DAnimation { namespace Animation { int componentsForType(int type) { int componentCount = 1; switch (type) { case QVariant::Double: componentCount = 1; break; case QVariant::Vector2D: componentCount = 2; break; case QVariant::Vector3D: componentCount = 3; break; case QVariant::Vector4D: case QVariant::Quaternion: componentCount = 4; break; default: qWarning() << "Unhandled animation type"; } return componentCount; } ClipEvaluationData evaluationDataForClip(AnimationClip *clip, const AnimatorEvaluationData &animatorData) { // global time values expected in seconds ClipEvaluationData result; result.localTime = localTimeFromGlobalTime(animatorData.globalTime, animatorData.startTime, 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 t_local = playbackRate * (t_global - t_start_global); double loopNumber = 0; if (loopCount == 1) { t_local = qBound(0.0, t_local, duration); } else if (loopCount < 0) { // Loops forever (void) std::modf(t_local / duration, &loopNumber); t_local = std::fmod(t_local, duration); } else { // N loops t_local = qBound(0.0, t_local, double(loopCount) * duration); (void) std::modf(t_local / duration, &loopNumber); t_local = std::fmod(t_local, duration); // Ensure we clamp to end of final loop if (loopNumber == loopCount) { loopNumber = loopCount - 1; t_local = duration; } } qCDebug(Jobs) << "t_global - t_start =" << t_global - t_start_global << "current loop =" << loopNumber << "t =" << t_local << "duration =" << duration; currentLoop = loopNumber; return t_local; } double phaseFromGlobalTime(double t_global, double t_start_global, double playbackRate, double duration, int loopCount, int ¤tLoop) { const double t_local = localTimeFromGlobalTime(t_global, t_start_global, playbackRate, duration, loopCount, currentLoop); return t_local / duration; } ComponentIndices channelComponentsToIndices(const Channel &channel, int dataType, int offset) { #if defined Q_COMPILER_UNIFORM_INIT static const QVector standardSuffixes = { 'X', 'Y', 'Z', 'W' }; static const QVector quaternionSuffixes = { 'W', 'X', 'Y', 'Z' }; #else static const QVector standardSuffixes = (QVector() << 'X' << 'Y' << 'Z' << 'W'); static const QVector quaternionSuffixes = (QVector() << 'W' << 'X' << 'Y' << 'Z'); #endif if (dataType != QVariant::Quaternion) return channelComponentsToIndicesHelper(channel, dataType, offset, standardSuffixes); else return channelComponentsToIndicesHelper(channel, dataType, offset, quaternionSuffixes); } ComponentIndices channelComponentsToIndicesHelper(const Channel &channel, int dataType, int offset, const QVector &suffixes) { const int expectedComponentCount = componentsForType(dataType); const int actualComponentCount = channel.channelComponents.size(); if (actualComponentCount != expectedComponentCount) { qWarning() << "Data type expects" << expectedComponentCount << "but found" << actualComponentCount << "components in the animation clip"; } ComponentIndices indices(expectedComponentCount); for (int i = 0; i < expectedComponentCount; ++i) { const QString &componentName = channel.channelComponents[i].name; char suffix = componentName.at(componentName.length() - 1).toLatin1(); int index = suffixes.indexOf(suffix); if (index != -1) indices[i] = index + offset; else indices[i] = -1; } return indices; } ClipResults evaluateClipAtLocalTime(AnimationClip *clip, float localTime) { QVector channelResults; Q_ASSERT(clip); // Ensure we have enough storage to hold the evaluations channelResults.resize(clip->channelCount()); // Iterate over channels and evaluate the fcurves const QVector &channels = clip->channels(); int i = 0; for (const Channel &channel : channels) { for (const auto &channelComponent : qAsConst(channel.channelComponents)) channelResults[i++] = channelComponent.fcurve.evaluateAtTime(localTime); } return channelResults; } ClipResults evaluateClipAtPhase(AnimationClip *clip, float phase) { // Calculate the clip local time from the phase and clip duration const double localTime = phase * clip->duration(); return evaluateClipAtLocalTime(clip, localTime); } QVector preparePropertyChanges(Qt3DCore::QNodeId animatorId, const QVector &mappingDataVec, const QVector &channelResults, bool finalFrame) { QVector changes; // Iterate over the mappings for (const MappingData &mappingData : mappingDataVec) { // Construct a property update change, set target, property and delivery options auto e = Qt3DCore::QPropertyUpdatedChangePtr::create(mappingData.targetId); e->setDeliveryFlags(Qt3DCore::QSceneChange::DeliverToAll); e->setPropertyName(mappingData.propertyName); // Handle intermediate updates vs final flag properly Qt3DCore::QPropertyUpdatedChangeBasePrivate::get(e.data())->m_isIntermediate = !finalFrame; // Build the new value from the channel/fcurve evaluation results QVariant v; switch (mappingData.type) { case QVariant::Double: { v = QVariant::fromValue(channelResults[mappingData.channelIndices[0]]); break; } case QVariant::Vector2D: { const QVector2D vector(channelResults[mappingData.channelIndices[0]], channelResults[mappingData.channelIndices[1]]); v = QVariant::fromValue(vector); break; } case QVariant::Vector3D: { const QVector3D vector(channelResults[mappingData.channelIndices[0]], channelResults[mappingData.channelIndices[1]], channelResults[mappingData.channelIndices[2]]); v = QVariant::fromValue(vector); break; } case QVariant::Vector4D: { const QVector4D vector(channelResults[mappingData.channelIndices[0]], channelResults[mappingData.channelIndices[1]], channelResults[mappingData.channelIndices[2]], channelResults[mappingData.channelIndices[3]]); v = QVariant::fromValue(vector); break; } case QVariant::Quaternion: { QQuaternion q(channelResults[mappingData.channelIndices[0]], channelResults[mappingData.channelIndices[1]], channelResults[mappingData.channelIndices[2]], channelResults[mappingData.channelIndices[3]]); q.normalize(); v = QVariant::fromValue(q); break; } default: qWarning() << "Unhandled animation type"; continue; } // Assign new value and send e->setValue(v); changes.push_back(e); } // If it's the final frame, notify the frontend that we've stopped if (finalFrame) { auto e = Qt3DCore::QPropertyUpdatedChangePtr::create(animatorId); e->setDeliveryFlags(Qt3DCore::QSceneChange::DeliverToAll); e->setPropertyName("running"); e->setValue(false); changes.push_back(e); } return changes; } //TODO: Remove this and use new implementation below for both the unblended // and blended animation cases. QVector buildPropertyMappings(Handler *handler, const AnimationClip *clip, const ChannelMapper *mapper) { QVector mappingDataVec; ChannelMappingManager *mappingManager = handler->channelMappingManager(); const QVector &channels = clip->channels(); // Iterate over the mappings in the mapper object const auto mappingIds = mapper->mappingIds(); for (const Qt3DCore::QNodeId mappingId : mappingIds) { // Get the mapping object ChannelMapping *mapping = mappingManager->lookupResource(mappingId); Q_ASSERT(mapping); // Populate the data we need, easy stuff first MappingData mappingData; mappingData.targetId = mapping->targetId(); mappingData.propertyName = mapping->propertyName(); mappingData.type = mapping->type(); if (mappingData.type == static_cast(QVariant::Invalid)) { qWarning() << "Unknown type for node id =" << mappingData.targetId << "and property =" << mapping->property(); continue; } // Now the tricky part. Mapping the channel indices onto the property type. // Try to find a ChannelGroup with matching name const QString channelName = mapping->channelName(); int channelGroupIndex = 0; bool foundMatch = false; for (const Channel &channel : channels) { if (channel.name == channelName) { foundMatch = true; const int channelBaseIndex = clip->channelComponentBaseIndex(channelGroupIndex); // Within this group, match channel names with index ordering mappingData.channelIndices = channelComponentsToIndices(channel, mappingData.type, channelBaseIndex); // Store the mapping data mappingDataVec.push_back(mappingData); if (foundMatch) break; } ++channelGroupIndex; } } return mappingDataVec; } QVector buildPropertyMappings(const QVector &channelMappings, const QVector &channelNamesAndTypes, const QVector &channelComponentIndices) { QVector mappingDataVec; mappingDataVec.reserve(channelMappings.size()); // Iterate over the mappings for (const auto mapping : channelMappings) { // Populate the data we need, easy stuff first MappingData mappingData; mappingData.targetId = mapping->targetId(); mappingData.propertyName = mapping->propertyName(); mappingData.type = mapping->type(); if (mappingData.type == static_cast(QVariant::Invalid)) { qWarning() << "Unknown type for node id =" << mappingData.targetId << "and property =" << mapping->property(); continue; } // Try to find matching channel name and type const ChannelNameAndType nameAndType = { mapping->channelName(), mapping->type() }; const int index = channelNamesAndTypes.indexOf(nameAndType); if (index != -1) { // We got one! mappingData.channelIndices = channelComponentIndices[index]; mappingDataVec.push_back(mappingData); } } return mappingDataVec; } QVector buildRequiredChannelsAndTypes(Handler *handler, const ChannelMapper *mapper) { ChannelMappingManager *mappingManager = handler->channelMappingManager(); const QVector mappingIds = mapper->mappingIds(); // Reserve enough storage assuming each mapping is for a different channel. // May be overkill but avoids potential for multiple allocations QVector namesAndTypes; namesAndTypes.reserve(mappingIds.size()); // Iterate through the mappings and add ones not already used by an earlier mapping. // We could add them all then sort and remove duplicates. However, our approach has the // advantage of keeping the blend tree format more consistent with the mapping // orderings which will have better cache locality when generating events. for (const Qt3DCore::QNodeId mappingId : mappingIds) { // Get the mapping object ChannelMapping *mapping = mappingManager->lookupResource(mappingId); Q_ASSERT(mapping); // Get the name and type const ChannelNameAndType nameAndType{ mapping->channelName(), mapping->type() }; // Add if not already contained if (!namesAndTypes.contains(nameAndType)) namesAndTypes.push_back(nameAndType); } return namesAndTypes; } QVector assignChannelComponentIndices(const QVector &namesAndTypes) { QVector channelComponentIndices; channelComponentIndices.reserve(namesAndTypes.size()); int baseIndex = 0; for (const auto &entry : namesAndTypes) { // Populate indices in order const int componentCount = componentsForType(entry.type); ComponentIndices indices(componentCount); std::iota(indices.begin(), indices.end(), baseIndex); // Append to the results channelComponentIndices.push_back(indices); // Increment baseIndex baseIndex += componentCount; } return channelComponentIndices; } QVector gatherValueNodesToEvaluate(Handler *handler, Qt3DCore::QNodeId blendTreeRootId) { Q_ASSERT(handler); Q_ASSERT(blendTreeRootId.isNull() == false); // We need the ClipBlendNodeManager to be able to lookup nodes from their Ids ClipBlendNodeManager *nodeManager = handler->clipBlendNodeManager(); // Visit the tree in a pre-order manner and collect the dependencies QVector clipIds; ClipBlendNodeVisitor visitor(nodeManager, ClipBlendNodeVisitor::PreOrder, ClipBlendNodeVisitor::VisitOnlyDependencies); auto func = [&clipIds, nodeManager] (ClipBlendNode *blendNode) { const auto dependencyIds = blendNode->currentDependencyIds(); for (const auto dependencyId : dependencyIds) { // Look up the blend node and if it's a value type (clip), // add it to the set of value node ids that need to be evaluated ClipBlendNode *node = nodeManager->lookupNode(dependencyId); if (node && node->blendType() == ClipBlendNode::ValueType) clipIds.append(dependencyId); } }; visitor.traverse(blendTreeRootId, func); // Sort and remove duplicates std::sort(clipIds.begin(), clipIds.end()); auto last = std::unique(clipIds.begin(), clipIds.end()); clipIds.erase(last, clipIds.end()); return clipIds; } ComponentIndices generateClipFormatIndices(const QVector &targetChannels, const QVector &targetIndices, const AnimationClip *clip) { Q_ASSERT(targetChannels.size() == targetIndices.size()); // Reserve enough storage for all the format indices int indexCount = 0; for (const auto targetIndexVec : qAsConst(targetIndices)) indexCount += targetIndexVec.size(); ComponentIndices format; format.resize(indexCount); // Iterate through the target channels const int channelCount = targetChannels.size(); auto formatIt = format.begin(); for (int i = 0; i < channelCount; ++i) { // Find the index of the channel from the clip const ChannelNameAndType &targetChannel = targetChannels[i]; const int clipChannelIndex = clip->channelIndex(targetChannel.name); // TODO: Ensure channel in the clip has enough components to map to the type. // Requires some improvements to the clip data structure first. // TODO: I don't think we need the targetIndices, only the number of components // for each target channel. Check once blend tree is complete. 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. const int baseIndex = clip->channelComponentBaseIndex(clipChannelIndex); std::iota(formatIt, formatIt + componentCount, baseIndex); } else { // No such channel in this clip. We'll use default values when // mapping from the clip to the formatted clip results. std::fill(formatIt, formatIt + componentCount, -1); } formatIt += componentCount; } return format; } ClipResults formatClipResults(const ClipResults &rawClipResults, const ComponentIndices &format) { // Resize the output to match the number of indices const int elementCount = format.size(); 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 for (int i = 0; i < elementCount; ++i) { const float value = format[i] != -1 ? rawClipResults[format[i]] : 0.0f; formattedClipResults[i] = value; } return formattedClipResults; } ClipResults evaluateBlendTree(Handler *handler, BlendedClipAnimator *animator, Qt3DCore::QNodeId blendTreeRootId) { Q_ASSERT(handler); Q_ASSERT(blendTreeRootId.isNull() == false); const Qt3DCore::QNodeId animatorId = animator->peerId(); // We need the ClipBlendNodeManager to be able to lookup nodes from their Ids ClipBlendNodeManager *nodeManager = handler->clipBlendNodeManager(); // Visit the tree in a post-order manner and for each interior node call // blending function. We only need to visit the nodes that affect the blend // tree at this time. ClipBlendNodeVisitor visitor(nodeManager, ClipBlendNodeVisitor::PostOrder, ClipBlendNodeVisitor::VisitOnlyDependencies); // TODO: When jobs can spawn other jobs we could evaluate subtrees of // the blend tree in parallel. Since it's just a dependency tree, it maps // simply onto the dependencies between jobs. auto func = [animatorId] (ClipBlendNode *blendNode) { // Look up the blend node and if it's an interior node, perform // the blend operation if (blendNode->blendType() != ClipBlendNode::ValueType) blendNode->blend(animatorId); }; visitor.traverse(blendTreeRootId, func); // The clip results stored in the root node for this animator // now represent the result of the blend tree evaluation ClipBlendNode *blendTreeRootNode = nodeManager->lookupNode(blendTreeRootId); Q_ASSERT(blendTreeRootNode); return blendTreeRootNode->clipResults(animatorId); } } // Animation } // Qt3DAnimation QT_END_NAMESPACE