| /**************************************************************************** |
| ** |
| ** 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 <Qt3DAnimation/private/handler_p.h> |
| #include <Qt3DAnimation/private/managers_p.h> |
| #include <Qt3DAnimation/private/clipblendnode_p.h> |
| #include <Qt3DAnimation/private/clipblendnodevisitor_p.h> |
| #include <Qt3DAnimation/private/clipblendvalue_p.h> |
| #include <QtGui/qvector2d.h> |
| #include <QtGui/qvector3d.h> |
| #include <QtGui/qvector4d.h> |
| #include <QtGui/qquaternion.h> |
| #include <QtGui/qcolor.h> |
| #include <QtCore/qvariant.h> |
| #include <QtCore/qvarlengtharray.h> |
| #include <Qt3DAnimation/private/animationlogging_p.h> |
| |
| #include <numeric> |
| |
| QT_BEGIN_NAMESPACE |
| |
| namespace { |
| const auto slerpThreshold = 0.01f; |
| } |
| |
| namespace Qt3DAnimation { |
| namespace Animation { |
| |
| inline QVector<float> valueToVector(const QVector3D &value) |
| { |
| return { value.x(), value.y(), value.z() }; |
| } |
| |
| inline QVector<float> valueToVector(const QQuaternion &value) |
| { |
| return { value.scalar(), value.x(), value.y(), value.z() }; |
| } |
| |
| ClipEvaluationData evaluationDataForClip(AnimationClip *clip, |
| const AnimatorEvaluationData &animatorData) |
| { |
| // global time values expected in seconds |
| ClipEvaluationData result; |
| result.currentLoop = animatorData.currentLoop; |
| result.localTime = localTimeFromElapsedTime(animatorData.currentTime, animatorData.elapsedTime, |
| animatorData.playbackRate, clip->duration(), |
| animatorData.loopCount, result.currentLoop); |
| result.isFinalFrame = isFinalFrame(result.localTime, clip->duration(), |
| result.currentLoop, animatorData.loopCount, |
| animatorData.playbackRate); |
| const bool hasNormalizedTime = isValidNormalizedTime(animatorData.normalizedLocalTime); |
| result.normalizedLocalTime = hasNormalizedTime ? animatorData.normalizedLocalTime |
| : result.localTime / clip->duration(); |
| return result; |
| } |
| |
| double localTimeFromElapsedTime(double t_current_local, |
| double t_elapsed_global, |
| double playbackRate, |
| double duration, |
| int loopCount, |
| int ¤tLoop) |
| { |
| // Calculate the new local time. |
| // playhead + rate * dt |
| // where playhead is completed loops * duration + current loop local time |
| double t_local = currentLoop * duration + t_current_local + playbackRate * t_elapsed_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 (int(loopNumber) == loopCount || int(loopNumber) < 0) { |
| loopNumber = loopCount - 1; |
| t_local = playbackRate >= 0.0 ? duration : 0.0; |
| } |
| } |
| |
| qCDebug(Jobs) << "current loop =" << loopNumber |
| << "t =" << t_local |
| << "duration =" << duration; |
| |
| currentLoop = int(loopNumber); |
| |
| return t_local; |
| } |
| |
| double phaseFromElapsedTime(double t_current_local, |
| double t_elapsed_global, |
| double playbackRate, |
| double duration, |
| int loopCount, |
| int ¤tLoop) |
| { |
| const double t_local = localTimeFromElapsedTime(t_current_local, t_elapsed_global, playbackRate, |
| duration, loopCount, currentLoop); |
| return t_local / duration; |
| } |
| |
| /*! |
| \internal |
| |
| Calculates the indices required to map from the component ordering within the |
| provided \a channel, into the standard channel orderings expected by Qt types. |
| |
| For example, given a channel representing a rotation with the components ordered |
| as X, Y, Z, Y, this function will return the indices [3, 0, 1, 2] which can then |
| later be used as part of the format vector in the formatClipResults() function to |
| remap the channels into the standard W, X, Y, Z order required by QQuaternion. |
| */ |
| ComponentIndices channelComponentsToIndices(const Channel &channel, |
| int dataType, |
| int expectedComponentCount, |
| int offset) |
| { |
| #if defined Q_COMPILER_UNIFORM_INIT |
| static const QVector<char> standardSuffixes = { 'X', 'Y', 'Z', 'W' }; |
| static const QVector<char> quaternionSuffixes = { 'W', 'X', 'Y', 'Z' }; |
| static const QVector<char> colorSuffixesRGB = { 'R', 'G', 'B' }; |
| static const QVector<char> colorSuffixesRGBA = { 'R', 'G', 'B', 'A' }; |
| #else |
| static const QVector<char> standardSuffixes = (QVector<char>() << 'X' << 'Y' << 'Z' << 'W'); |
| static const QVector<char> quaternionSuffixes = (QVector<char>() << 'W' << 'X' << 'Y' << 'Z'); |
| static const QVector<char> colorSuffixesRGB = (QVector<char>() << 'R' << 'G' << 'B'); |
| static const QVector<char> colorSuffixesRGBA = (QVector<char>() << 'R' << 'G' << 'B' << 'A'); |
| #endif |
| |
| switch (dataType) { |
| case QVariant::Quaternion: |
| return channelComponentsToIndicesHelper(channel, expectedComponentCount, |
| offset, quaternionSuffixes); |
| case QVariant::Color: |
| if (expectedComponentCount == 3) |
| return channelComponentsToIndicesHelper(channel, expectedComponentCount, |
| offset, colorSuffixesRGB); |
| Q_ASSERT(expectedComponentCount == 4); |
| return channelComponentsToIndicesHelper(channel, expectedComponentCount, |
| offset, colorSuffixesRGBA); |
| default: |
| return channelComponentsToIndicesHelper(channel, expectedComponentCount, |
| offset, standardSuffixes); |
| } |
| } |
| |
| ComponentIndices channelComponentsToIndicesHelper(const Channel &channel, |
| int expectedComponentCount, |
| int offset, |
| const QVector<char> &suffixes) |
| { |
| 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); |
| |
| // Generate the set of channel suffixes |
| QVector<char> channelSuffixes; |
| channelSuffixes.reserve(expectedComponentCount); |
| for (int i = 0; i < expectedComponentCount; ++i) { |
| const QString &componentName = channel.channelComponents[i].name; |
| |
| // An unset component name indicates that the no mapping is necessary |
| // and the index can be used as-is. |
| if (componentName.isEmpty()) { |
| indices[i] = i + offset; |
| continue; |
| } |
| |
| char channelSuffix = componentName.at(componentName.length() - 1).toLatin1(); |
| channelSuffixes.push_back(channelSuffix); |
| } |
| |
| // We can short-circuit if the channels were all unnamed (in order) |
| if (channelSuffixes.isEmpty()) |
| return indices; |
| |
| // Find index of standard index in channel indexes |
| for (int i = 0; i < expectedComponentCount; ++i) { |
| int index = channelSuffixes.indexOf(suffixes[i]); |
| if (index != -1) |
| indices[i] = index + offset; |
| else |
| indices[i] = -1; |
| } |
| |
| return indices; |
| } |
| |
| ClipResults evaluateClipAtLocalTime(AnimationClip *clip, float localTime) |
| { |
| QVector<float> 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<Channel> &channels = clip->channels(); |
| int i = 0; |
| for (const Channel &channel : channels) { |
| if (channel.name.contains(QStringLiteral("Rotation")) && |
| channel.channelComponents.size() == 4) { |
| |
| // Try to SLERP |
| const int nbKeyframes = channel.channelComponents[0].fcurve.keyframeCount(); |
| const bool canSlerp = std::find_if(std::begin(channel.channelComponents)+1, |
| std::end(channel.channelComponents), |
| [nbKeyframes](const ChannelComponent &v) { |
| return v.fcurve.keyframeCount() != nbKeyframes; |
| }) == std::end(channel.channelComponents); |
| |
| if (!canSlerp) { |
| // Interpolate per component |
| for (const auto &channelComponent : qAsConst(channel.channelComponents)) { |
| const int lowerKeyframeBound = channelComponent.fcurve.lowerKeyframeBound(localTime); |
| channelResults[i++] = channelComponent.fcurve.evaluateAtTime(localTime, lowerKeyframeBound); |
| } |
| } else { |
| // There's only one keyframe. We cant compute omega. Interpolate per component |
| if (channel.channelComponents[0].fcurve.keyframeCount() == 1) { |
| for (const auto &channelComponent : qAsConst(channel.channelComponents)) |
| channelResults[i++] = channelComponent.fcurve.keyframe(0).value; |
| } else { |
| auto quaternionFromChannel = [channel](const int keyframe) { |
| const float w = channel.channelComponents[0].fcurve.keyframe(keyframe).value; |
| const float x = channel.channelComponents[1].fcurve.keyframe(keyframe).value; |
| const float y = channel.channelComponents[2].fcurve.keyframe(keyframe).value; |
| const float z = channel.channelComponents[3].fcurve.keyframe(keyframe).value; |
| QQuaternion quat{w,x,y,z}; |
| quat.normalize(); |
| return quat; |
| }; |
| |
| const int lowerKeyframeBound = channel.channelComponents[0].fcurve.lowerKeyframeBound(localTime); |
| const auto lowerQuat = quaternionFromChannel(lowerKeyframeBound); |
| const auto higherQuat = quaternionFromChannel(lowerKeyframeBound + 1); |
| auto cosHalfTheta = QQuaternion::dotProduct(lowerQuat, higherQuat); |
| // If the two keyframe quaternions are equal, just return the first one as the interpolated value. |
| if (std::abs(cosHalfTheta) >= 1.0f) { |
| channelResults[i++] = lowerQuat.scalar(); |
| channelResults[i++] = lowerQuat.x(); |
| channelResults[i++] = lowerQuat.y(); |
| channelResults[i++] = lowerQuat.z(); |
| } else { |
| const auto sinHalfTheta = std::sqrt(1.0f - std::pow(cosHalfTheta,2.0f)); |
| if (std::abs(sinHalfTheta) < ::slerpThreshold) { |
| auto initial_i = i; |
| for (const auto &channelComponent : qAsConst(channel.channelComponents)) |
| channelResults[i++] = channelComponent.fcurve.evaluateAtTime(localTime, lowerKeyframeBound); |
| |
| // Normalize the resulting quaternion |
| QQuaternion quat{channelResults[initial_i], channelResults[initial_i+1], channelResults[initial_i+2], channelResults[initial_i+3]}; |
| quat.normalize(); |
| channelResults[initial_i+0] = quat.scalar(); |
| channelResults[initial_i+1] = quat.x(); |
| channelResults[initial_i+2] = quat.y(); |
| channelResults[initial_i+3] = quat.z(); |
| } else { |
| const auto reverseQ1 = cosHalfTheta < 0 ? -1.0f : 1.0f; |
| cosHalfTheta *= reverseQ1; |
| const auto halfTheta = std::acos(cosHalfTheta); |
| for (const auto &channelComponent : qAsConst(channel.channelComponents)) |
| channelResults[i++] = channelComponent.fcurve.evaluateAtTimeAsSlerp(localTime, |
| lowerKeyframeBound, |
| halfTheta, |
| sinHalfTheta, |
| reverseQ1); |
| } |
| } |
| } |
| } |
| } else { |
| // If the channel is not a Rotation, apply linear interpolation per channel component |
| // TODO How do we handle other interpolations. For exammple, color interpolation |
| // in a linear perceptual way or other non linear spaces? |
| for (const auto &channelComponent : qAsConst(channel.channelComponents)) { |
| const int lowerKeyframeBound = channelComponent.fcurve.lowerKeyframeBound(localTime); |
| channelResults[i++] = channelComponent.fcurve.evaluateAtTime(localTime, lowerKeyframeBound); |
| } |
| } |
| } |
| 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); |
| } |
| |
| template<typename Container> |
| Container mapChannelResultsToContainer(const MappingData &mappingData, |
| const QVector<float> &channelResults) |
| { |
| Container r; |
| r.reserve(channelResults.size()); |
| |
| const ComponentIndices channelIndices = mappingData.channelIndices; |
| for (const int channelIndex : channelIndices) |
| r.push_back(channelResults.at(channelIndex)); |
| |
| return r; |
| } |
| |
| QVariant buildPropertyValue(const MappingData &mappingData, const QVector<float> &channelResults) |
| { |
| const int vectorOfFloatType = qMetaTypeId<QVector<float>>(); |
| |
| if (mappingData.type == vectorOfFloatType) |
| return QVariant::fromValue(channelResults); |
| |
| switch (mappingData.type) { |
| case QMetaType::Float: |
| case QVariant::Double: { |
| return QVariant::fromValue(channelResults[mappingData.channelIndices[0]]); |
| } |
| |
| case QVariant::Vector2D: { |
| const QVector2D vector(channelResults[mappingData.channelIndices[0]], |
| channelResults[mappingData.channelIndices[1]]); |
| return QVariant::fromValue(vector); |
| } |
| |
| case QVariant::Vector3D: { |
| const QVector3D vector(channelResults[mappingData.channelIndices[0]], |
| channelResults[mappingData.channelIndices[1]], |
| channelResults[mappingData.channelIndices[2]]); |
| return QVariant::fromValue(vector); |
| } |
| |
| case QVariant::Vector4D: { |
| const QVector4D vector(channelResults[mappingData.channelIndices[0]], |
| channelResults[mappingData.channelIndices[1]], |
| channelResults[mappingData.channelIndices[2]], |
| channelResults[mappingData.channelIndices[3]]); |
| return QVariant::fromValue(vector); |
| } |
| |
| case QVariant::Quaternion: { |
| QQuaternion q(channelResults[mappingData.channelIndices[0]], |
| channelResults[mappingData.channelIndices[1]], |
| channelResults[mappingData.channelIndices[2]], |
| channelResults[mappingData.channelIndices[3]]); |
| q.normalize(); |
| return QVariant::fromValue(q); |
| } |
| |
| case QVariant::Color: { |
| // A color can either be a vec3 or a vec4 |
| const QColor color = |
| QColor::fromRgbF(channelResults[mappingData.channelIndices[0]], |
| channelResults[mappingData.channelIndices[1]], |
| channelResults[mappingData.channelIndices[2]], |
| mappingData.channelIndices.size() > 3 ? channelResults[mappingData.channelIndices[3]] : 1.0f); |
| return QVariant::fromValue(color); |
| } |
| |
| case QVariant::List: { |
| const QVariantList results = mapChannelResultsToContainer<QVariantList>( |
| mappingData, channelResults); |
| return QVariant::fromValue(results); |
| } |
| default: |
| qWarning() << "Unhandled animation type" << mappingData.type; |
| break; |
| } |
| |
| return QVariant(); |
| } |
| |
| AnimationRecord prepareAnimationRecord(Qt3DCore::QNodeId animatorId, |
| const QVector<MappingData> &mappingDataVec, |
| const QVector<float> &channelResults, |
| bool finalFrame, |
| float normalizedLocalTime) |
| { |
| AnimationRecord record; |
| record.finalFrame = finalFrame; |
| record.animatorId = animatorId; |
| record.normalizedTime = normalizedLocalTime; |
| |
| QVarLengthArray<Skeleton *, 4> dirtySkeletons; |
| |
| // Iterate over the mappings |
| for (const MappingData &mappingData : mappingDataVec) { |
| if (!mappingData.propertyName) |
| continue; |
| |
| // Build the new value from the channel/fcurve evaluation results |
| const QVariant v = buildPropertyValue(mappingData, channelResults); |
| if (!v.isValid()) |
| continue; |
| |
| // TODO: Avoid wrapping joint transform components up in a variant, just |
| // to immediately unwrap them again. Refactor buildPropertyValue() to call |
| // helper functions that we can call directly here for joints. |
| if (mappingData.skeleton && mappingData.jointIndex != -1) { |
| // Remember that this skeleton is dirty. We will ask each dirty skeleton |
| // to send its set of local poses to observers below. |
| if (!dirtySkeletons.contains(mappingData.skeleton)) |
| dirtySkeletons.push_back(mappingData.skeleton); |
| |
| switch (mappingData.jointTransformComponent) { |
| case Scale: |
| mappingData.skeleton->setJointScale(mappingData.jointIndex, v.value<QVector3D>()); |
| break; |
| |
| case Rotation: |
| mappingData.skeleton->setJointRotation(mappingData.jointIndex, v.value<QQuaternion>()); |
| break; |
| |
| case Translation: |
| mappingData.skeleton->setJointTranslation(mappingData.jointIndex, v.value<QVector3D>()); |
| break; |
| |
| default: |
| Q_UNREACHABLE(); |
| break; |
| } |
| } else { |
| record.targetChanges.push_back({mappingData.targetId, mappingData.propertyName, v}); |
| } |
| } |
| |
| for (const auto skeleton : dirtySkeletons) |
| record.skeletonChanges.push_back({skeleton->peerId(), skeleton->joints()}); |
| |
| return record; |
| } |
| |
| QVector<AnimationCallbackAndValue> prepareCallbacks(const QVector<MappingData> &mappingDataVec, |
| const QVector<float> &channelResults) |
| { |
| QVector<AnimationCallbackAndValue> callbacks; |
| for (const MappingData &mappingData : mappingDataVec) { |
| if (!mappingData.callback) |
| continue; |
| const QVariant v = buildPropertyValue(mappingData, channelResults); |
| if (v.isValid()) { |
| AnimationCallbackAndValue callback; |
| callback.callback = mappingData.callback; |
| callback.flags = mappingData.callbackFlags; |
| callback.value = v; |
| callbacks.append(callback); |
| } |
| } |
| return callbacks; |
| } |
| |
| // TODO: Optimize this even more by combining the work done here with the functions: |
| // buildRequiredChannelsAndTypes() and assignChannelComponentIndices(). We are |
| // currently repeating the iteration over mappings and extracting/generating |
| // channel names, types and joint indices. |
| QVector<MappingData> buildPropertyMappings(const QVector<ChannelMapping*> &channelMappings, |
| const QVector<ChannelNameAndType> &channelNamesAndTypes, |
| const QVector<ComponentIndices> &channelComponentIndices, |
| const QVector<QBitArray> &sourceClipMask) |
| { |
| // Accumulate the required number of mappings |
| int maxMappingDatas = 0; |
| for (const auto mapping : channelMappings) { |
| switch (mapping->mappingType()) { |
| case ChannelMapping::ChannelMappingType: |
| case ChannelMapping::CallbackMappingType: |
| ++maxMappingDatas; |
| break; |
| |
| case ChannelMapping::SkeletonMappingType: { |
| Skeleton *skeleton = mapping->skeleton(); |
| maxMappingDatas += 3 * skeleton->jointCount(); // S, R, T |
| break; |
| } |
| } |
| } |
| QVector<MappingData> mappingDataVec; |
| mappingDataVec.reserve(maxMappingDatas); |
| |
| // Iterate over the mappings |
| for (const auto mapping : channelMappings) { |
| switch (mapping->mappingType()) { |
| case ChannelMapping::ChannelMappingType: |
| case ChannelMapping::CallbackMappingType: { |
| // Populate the data we need, easy stuff first |
| MappingData mappingData; |
| mappingData.targetId = mapping->targetId(); |
| mappingData.propertyName = mapping->propertyName(); |
| mappingData.type = mapping->type(); |
| mappingData.callback = mapping->callback(); |
| mappingData.callbackFlags = mapping->callbackFlags(); |
| |
| if (mappingData.type == static_cast<int>(QVariant::Invalid)) { |
| qWarning() << "Unknown type for node id =" << mappingData.targetId |
| << "and property =" << mapping->propertyName() |
| << "and callback =" << mapping->callback(); |
| continue; |
| } |
| |
| // Try to find matching channel name and type |
| const ChannelNameAndType nameAndType = { mapping->channelName(), |
| mapping->type(), |
| mapping->componentCount(), |
| mapping->peerId() |
| }; |
| const int index = channelNamesAndTypes.indexOf(nameAndType); |
| if (index != -1) { |
| // Do we have any animation data for this channel? If not, don't bother |
| // adding a mapping for it. |
| const bool hasChannelIndices = sourceClipMask[index].count(true) != 0; |
| if (!hasChannelIndices) |
| continue; |
| |
| // We got one! |
| mappingData.channelIndices = channelComponentIndices[index]; |
| mappingDataVec.push_back(mappingData); |
| } |
| break; |
| } |
| |
| case ChannelMapping::SkeletonMappingType: { |
| const QVector<ChannelNameAndType> jointProperties |
| = { { QLatin1String("Location"), static_cast<int>(QVariant::Vector3D), Translation }, |
| { QLatin1String("Rotation"), static_cast<int>(QVariant::Quaternion), Rotation }, |
| { QLatin1String("Scale"), static_cast<int>(QVariant::Vector3D), Scale } }; |
| const QHash<QString, const char *> channelNameToPropertyName |
| = { { QLatin1String("Location"), "translation" }, |
| { QLatin1String("Rotation"), "rotation" }, |
| { QLatin1String("Scale"), "scale" } }; |
| Skeleton *skeleton = mapping->skeleton(); |
| const int jointCount = skeleton->jointCount(); |
| for (int jointIndex = 0; jointIndex < jointCount; ++jointIndex) { |
| // Populate the data we need, easy stuff first |
| MappingData mappingData; |
| mappingData.targetId = mapping->skeletonId(); |
| mappingData.skeleton = mapping->skeleton(); |
| |
| const int propertyCount = jointProperties.size(); |
| for (int propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex) { |
| // Get the name, type and index |
| ChannelNameAndType nameAndType = jointProperties[propertyIndex]; |
| nameAndType.jointIndex = jointIndex; |
| nameAndType.mappingId = mapping->peerId(); |
| |
| // Try to find matching channel name and type |
| const int index = channelNamesAndTypes.indexOf(nameAndType); |
| if (index == -1) |
| continue; |
| |
| // Do we have any animation data for this channel? If not, don't bother |
| // adding a mapping for it. |
| const bool hasChannelIndices = sourceClipMask[index].count(true) != 0; |
| if (!hasChannelIndices) |
| continue; |
| |
| if (index != -1) { |
| // We got one! |
| mappingData.propertyName = channelNameToPropertyName[nameAndType.name]; |
| mappingData.type = nameAndType.type; |
| mappingData.channelIndices = channelComponentIndices[index]; |
| mappingData.jointIndex = jointIndex; |
| |
| // Convert property name for joint transform components to |
| // an enumerated type so we can avoid the string comparisons |
| // when sending the change events after evaluation. |
| // TODO: Replace this logic as we now do it in buildRequiredChannelsAndTypes() |
| if (qstrcmp(mappingData.propertyName, "scale") == 0) |
| mappingData.jointTransformComponent = Scale; |
| else if (qstrcmp(mappingData.propertyName, "rotation") == 0) |
| mappingData.jointTransformComponent = Rotation; |
| else if (qstrcmp(mappingData.propertyName, "translation") == 0) |
| mappingData.jointTransformComponent = Translation; |
| |
| mappingDataVec.push_back(mappingData); |
| } |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| return mappingDataVec; |
| } |
| |
| QVector<ChannelNameAndType> buildRequiredChannelsAndTypes(Handler *handler, |
| const ChannelMapper *mapper) |
| { |
| ChannelMappingManager *mappingManager = handler->channelMappingManager(); |
| const QVector<Qt3DCore::QNodeId> mappingIds = mapper->mappingIds(); |
| |
| // Reserve enough storage assuming each mapping is for a different channel. |
| // May be overkill but avoids potential for multiple allocations |
| QVector<ChannelNameAndType> 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); |
| |
| switch (mapping->mappingType()) { |
| case ChannelMapping::ChannelMappingType: |
| case ChannelMapping::CallbackMappingType: { |
| // Get the name and type |
| const ChannelNameAndType nameAndType{ mapping->channelName(), |
| mapping->type(), |
| mapping->componentCount(), |
| mappingId }; |
| |
| // Add if not already contained |
| if (!namesAndTypes.contains(nameAndType)) |
| namesAndTypes.push_back(nameAndType); |
| |
| break; |
| } |
| |
| case ChannelMapping::SkeletonMappingType: { |
| // Add an entry for each scale/rotation/translation property of each joint index |
| // of the target skeleton. |
| const QVector<ChannelNameAndType> jointProperties |
| = { { QLatin1String("Location"), static_cast<int>(QVariant::Vector3D), Translation }, |
| { QLatin1String("Rotation"), static_cast<int>(QVariant::Quaternion), Rotation }, |
| { QLatin1String("Scale"), static_cast<int>(QVariant::Vector3D), Scale } }; |
| Skeleton *skeleton = handler->skeletonManager()->lookupResource(mapping->skeletonId()); |
| const int jointCount = skeleton->jointCount(); |
| for (int jointIndex = 0; jointIndex < jointCount; ++jointIndex) { |
| const int propertyCount = jointProperties.size(); |
| for (int propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex) { |
| // Get the name, type and index |
| ChannelNameAndType nameAndType = jointProperties[propertyIndex]; |
| nameAndType.jointName = skeleton->jointName(jointIndex); |
| nameAndType.jointIndex = jointIndex; |
| nameAndType.mappingId = mappingId; |
| |
| // Add if not already contained |
| if (!namesAndTypes.contains(nameAndType)) |
| namesAndTypes.push_back(nameAndType); |
| } |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| return namesAndTypes; |
| } |
| |
| QVector<ComponentIndices> assignChannelComponentIndices(const QVector<ChannelNameAndType> &namesAndTypes) |
| { |
| QVector<ComponentIndices> channelComponentIndices; |
| channelComponentIndices.reserve(namesAndTypes.size()); |
| |
| int baseIndex = 0; |
| for (const auto &entry : namesAndTypes) { |
| // Populate indices in order |
| const int componentCount = entry.componentCount; |
| 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<Qt3DCore::QNodeId> 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<Qt3DCore::QNodeId> clipIds; |
| ClipBlendNodeVisitor visitor(nodeManager, |
| ClipBlendNodeVisitor::PreOrder, |
| ClipBlendNodeVisitor::VisitOnlyDependencies); |
| |
| auto func = [&clipIds, nodeManager] (ClipBlendNode *blendNode) { |
| // Check if this is a value node itself |
| if (blendNode->blendType() == ClipBlendNode::ValueType) |
| clipIds.append(blendNode->peerId()); |
| |
| 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; |
| } |
| |
| ClipFormat generateClipFormatIndices(const QVector<ChannelNameAndType> &targetChannels, |
| const QVector<ComponentIndices> &targetIndices, |
| const AnimationClip *clip) |
| { |
| Q_ASSERT(targetChannels.size() == targetIndices.size()); |
| |
| // Reserve enough storage for all the format indices |
| const int channelCount = targetChannels.size(); |
| ClipFormat f; |
| f.namesAndTypes.resize(channelCount); |
| f.formattedComponentIndices.resize(channelCount); |
| f.sourceClipMask.resize(channelCount); |
| int indexCount = 0; |
| for (const auto &targetIndexVec : qAsConst(targetIndices)) |
| indexCount += targetIndexVec.size(); |
| ComponentIndices &sourceIndices = f.sourceClipIndices; |
| sourceIndices.resize(indexCount); |
| |
| // Iterate through the target channels |
| auto formatIt = sourceIndices.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, |
| targetChannel.jointIndex); |
| const int componentCount = targetIndices[i].size(); |
| |
| if (clipChannelIndex != -1) { |
| // Found a matching channel in the clip. Populate the corresponding |
| // entries in the format vector with the *source indices* |
| // needed to build the formatted results. |
| const int baseIndex = clip->channelComponentBaseIndex(clipChannelIndex); |
| const auto channelIndices = channelComponentsToIndices(clip->channels()[clipChannelIndex], |
| targetChannel.type, |
| targetChannel.componentCount, |
| baseIndex); |
| std::copy(channelIndices.begin(), channelIndices.end(), formatIt); |
| |
| f.sourceClipMask[i].resize(componentCount); |
| for (int j = 0; j < componentCount; ++j) |
| f.sourceClipMask[i].setBit(j, channelIndices[j] != -1); |
| } 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); |
| f.sourceClipMask[i].fill(false, componentCount); |
| } |
| |
| f.formattedComponentIndices[i] = targetIndices[i]; |
| f.namesAndTypes[i] = targetChannels[i]; |
| formatIt += componentCount; |
| } |
| |
| return f; |
| } |
| |
| 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 |
| // TODO: We could potentially avoid having holes in these intermediate |
| // vectors by adjusting the component indices stored in the MappingData |
| // and format vectors. Needs careful investigation! |
| for (int i = 0; i < elementCount; ++i) { |
| if (format[i] == -1) |
| continue; |
| formattedClipResults[i] = rawClipResults[format[i]]; |
| } |
| |
| 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); |
| } |
| |
| QVector<float> defaultValueForChannel(Handler *handler, |
| const ChannelNameAndType &channelDescription) |
| { |
| QVector<float> result; |
| |
| // Does the channel repesent a joint in a skeleton or is it a general channel? |
| ChannelMappingManager *mappingManager = handler->channelMappingManager(); |
| const ChannelMapping *mapping = mappingManager->lookupResource(channelDescription.mappingId); |
| switch (mapping->mappingType()) { |
| case ChannelMapping::SkeletonMappingType: { |
| // Default channel values for a joint in a skeleton, should be taken |
| // from the default pose of the joint itself. I.e. if a joint is not |
| // explicitly animated, then it should retain it's initial rest pose. |
| Skeleton *skeleton = mapping->skeleton(); |
| const int jointIndex = channelDescription.jointIndex; |
| switch (channelDescription.jointTransformComponent) { |
| case Translation: |
| result = valueToVector(skeleton->jointTranslation(jointIndex)); |
| break; |
| |
| case Rotation: |
| result = valueToVector(skeleton->jointRotation(jointIndex)); |
| break; |
| |
| case Scale: |
| result = valueToVector(skeleton->jointScale(jointIndex)); |
| break; |
| |
| case NoTransformComponent: |
| Q_UNREACHABLE(); |
| break; |
| } |
| break; |
| } |
| |
| case ChannelMapping::ChannelMappingType: |
| case ChannelMapping::CallbackMappingType: { |
| // Do our best to provide a sensible default value. |
| if (channelDescription.type == QMetaType::QQuaternion) { |
| result = valueToVector(QQuaternion()); // (1, 0, 0, 0) |
| break; |
| } |
| |
| if (channelDescription.name.toLower() == QLatin1String("scale")) { |
| result = valueToVector(QVector3D(1.0f, 1.0f, 1.0f)); |
| break; |
| } |
| |
| // Everything else gets all zeros |
| const int componentCount = mapping->componentCount(); |
| result = QVector<float>(componentCount, 0.0f); |
| break; |
| } |
| |
| } |
| |
| return result; |
| } |
| |
| void applyComponentDefaultValues(const QVector<ComponentValue> &componentDefaults, |
| ClipResults &formattedClipResults) |
| { |
| for (const auto &componentDefault : componentDefaults) |
| formattedClipResults[componentDefault.componentIndex] = componentDefault.value; |
| } |
| |
| } // Animation |
| } // Qt3DAnimation |
| |
| QT_END_NAMESPACE |