Revision control
++ // Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #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,
++ qsizetype expectedComponentCount,
++ qsizetype offset)
++ {
++ static const QList<char> standardSuffixes = { 'X', 'Y', 'Z', 'W' };
++ static const QList<char> quaternionSuffixes = { 'W', 'X', 'Y', 'Z' };
++ static const QList<char> colorSuffixesRGB = { 'R', 'G', 'B' };
++ static const QList<char> colorSuffixesRGBA = { 'R', 'G', 'B', 'A' };
++
++ switch (dataType) {
++ case QMetaType::QQuaternion:
++ return channelComponentsToIndicesHelper(channel, expectedComponentCount,
++ offset, quaternionSuffixes);
++ case QMetaType::QColor:
++ 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,
++ qsizetype expectedComponentCount,
++ qsizetype offset,
++ const QList<char> &suffixes)
++ {
++ const qsizetype 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
++ QList<char> channelSuffixes;
++ channelSuffixes.reserve(expectedComponentCount);
++ for (qsizetype 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.size() - 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 (qsizetype i = 0; i < expectedComponentCount; ++i) {
++ qsizetype 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 auto &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 : std::as_const(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 : std::as_const(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 = std::max(0, 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 : std::as_const(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 : std::as_const(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 : std::as_const(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<QList<float>>();
++
++ if (mappingData.type == vectorOfFloatType)
++ return QVariant::fromValue(channelResults);
++
++ switch (mappingData.type) {
++ case QMetaType::Float:
++ case QMetaType::Double: {
++ return QVariant::fromValue(channelResults[mappingData.channelIndices[0]]);
++ }
++
++ case QMetaType::QVector2D: {
++ const QVector2D vector(channelResults[mappingData.channelIndices[0]],
++ channelResults[mappingData.channelIndices[1]]);
++ return QVariant::fromValue(vector);
++ }
++
++ case QMetaType::QVector3D: {
++ const QVector3D vector(channelResults[mappingData.channelIndices[0]],
++ channelResults[mappingData.channelIndices[1]],
++ channelResults[mappingData.channelIndices[2]]);
++ return QVariant::fromValue(vector);
++ }
++
++ case QMetaType::QVector4D: {
++ const QVector4D vector(channelResults[mappingData.channelIndices[0]],
++ channelResults[mappingData.channelIndices[1]],
++ channelResults[mappingData.channelIndices[2]],
++ channelResults[mappingData.channelIndices[3]]);
++ return QVariant::fromValue(vector);
++ }
++
++ case QMetaType::QQuaternion: {
++ 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 QMetaType::QColor: {
++ // 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 QMetaType::QVariantList: {
++ 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>(QMetaType::UnknownType)) {
++ 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 qsizetype 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 QList<ChannelNameAndType> jointProperties
++ = { { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), Translation },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), Rotation },
++ { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), 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 qsizetype propertyCount = jointProperties.size();
++ for (qsizetype 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 qsizetype 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 auto 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 QList<ChannelNameAndType> jointProperties
++ = { { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), Translation },
++ { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), Rotation },
++ { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), Scale } };
++ Skeleton *skeleton = handler->skeletonManager()->lookupResource(mapping->skeletonId());
++ const int jointCount = skeleton->jointCount();
++ for (int jointIndex = 0; jointIndex < jointCount; ++jointIndex) {
++ const qsizetype 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 qsizetype channelCount = targetChannels.size();
++ ClipFormat f;
++ f.namesAndTypes.resize(channelCount);
++ f.formattedComponentIndices.resize(channelCount);
++ f.sourceClipMask.resize(channelCount);
++ qsizetype indexCount = 0;
++ for (const auto &targetIndexVec : std::as_const(targetIndices))
++ indexCount += targetIndexVec.size();
++ ComponentIndices &sourceIndices = f.sourceClipIndices;
++ sourceIndices.resize(indexCount);
++
++ // Iterate through the target channels
++ auto formatIt = sourceIndices.begin();
++ for (qsizetype i = 0; i < channelCount; ++i) {
++ // Find the index of the channel from the clip
++ const ChannelNameAndType &targetChannel = targetChannels[i];
++ const qsizetype clipChannelIndex = clip->channelIndex(targetChannel.name,
++ targetChannel.jointIndex);
++ const qsizetype 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 qsizetype 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 (qsizetype 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 qsizetype 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 (qsizetype 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