summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSean Harmer <sean.harmer@kdab.com>2017-03-15 18:38:45 +0000
committerSean Harmer <sean.harmer@kdab.com>2017-03-25 14:24:08 +0000
commitf743cf2833f42c8ac2c6c8ce5a198bbb5e3050e6 (patch)
treef7a93b8c85c1012ddf64e1c9d03d13bd5535cac2 /src
parent14538a90120d7d1e8f37c0740d8a6a669389caad (diff)
Implement the new blend tree prep and evaluation jobs
Change-Id: Ic619cfdc27d7c3c46d96d3cc0e83e9134ebb0216 Reviewed-by: Mike Krus <mike.krus@kdab.com>
Diffstat (limited to 'src')
-rw-r--r--src/animation/backend/buildblendtreesjob.cpp193
-rw-r--r--src/animation/backend/evaluateblendclipanimatorjob.cpp200
-rw-r--r--src/animation/backend/evaluateblendclipanimatorjob_p.h8
3 files changed, 100 insertions, 301 deletions
diff --git a/src/animation/backend/buildblendtreesjob.cpp b/src/animation/backend/buildblendtreesjob.cpp
index ef8c82ce1..d2fd5c03e 100644
--- a/src/animation/backend/buildblendtreesjob.cpp
+++ b/src/animation/backend/buildblendtreesjob.cpp
@@ -39,6 +39,7 @@
#include <Qt3DAnimation/private/managers_p.h>
#include <Qt3DAnimation/private/clipblendnodevisitor_p.h>
#include <Qt3DAnimation/private/clipblendnode_p.h>
+#include <Qt3DAnimation/private/clipblendvalue_p.h>
#include <Qt3DAnimation/private/lerpclipblend_p.h>
#include <Qt3DAnimation/private/job_common_p.h>
@@ -58,116 +59,6 @@ void BuildBlendTreesJob::setBlendedClipAnimators(const QVector<HBlendedClipAnima
m_blendedClipAnimatorHandles = blendedClipAnimatorHandles;
}
-namespace {
-
-template<typename MappingDataType>
-QVector<BlendingMappingData> buildBlendMappingDataForNode(const QVector<MappingDataType> &mappingInNode1,
- const QVector<MappingDataType> &mappingInNode2)
-{
- // We can only blend channels that are in both clips
- // If a channel is present in one clip and not the other, we use 100% of its value (no blending)
- QVector<BlendingMappingData> blendingMappingData;
- const int mappingInNode1Size = mappingInNode1.size();
- blendingMappingData.reserve(mappingInNode1Size);
-
- // Find mappings that are in both vectors and build mappingData out of that
- for (const MappingDataType &mappingDataInNode1 : mappingInNode1) {
- BlendingMappingData mappingData;
- mappingData.channelIndicesClip1 = mappingDataInNode1.channelIndices;
- mappingData.propertyName = mappingDataInNode1.propertyName;
- mappingData.targetId = mappingDataInNode1.targetId;
- mappingData.type = mappingDataInNode1.type;
- mappingData.blendAction = BlendingMappingData::NoBlending;
- blendingMappingData.push_back(mappingData);
- }
-
- for (const MappingDataType &mappingDataInNode2 : mappingInNode2) {
- bool sharedChannel = false;
- for (int i = 0; i < mappingInNode1Size; ++i) {
- BlendingMappingData &mappingDataInNode1 = blendingMappingData[i];
- if ((strcmp(mappingDataInNode1.propertyName, mappingDataInNode2.propertyName) == 0) &&
- mappingDataInNode1.targetId == mappingDataInNode2.targetId &&
- mappingDataInNode1.type == mappingDataInNode2.type) {
- // We have a channel shared in both clips
- mappingDataInNode1.channelIndicesClip2 = mappingDataInNode2.channelIndices;
- mappingDataInNode1.blendAction = BlendingMappingData::ClipBlending;
- sharedChannel = true;
- break;
- }
- }
- if (!sharedChannel) { // We have a channel defined in only one of the clips
- BlendingMappingData mappingData;
- mappingData.channelIndicesClip2 = mappingDataInNode2.channelIndices;
- mappingData.propertyName = mappingDataInNode2.propertyName;
- mappingData.targetId = mappingDataInNode2.targetId;
- mappingData.type = mappingDataInNode2.type;
- mappingData.blendAction = BlendingMappingData::NoBlending;
- blendingMappingData.push_back(mappingData);
- }
- }
-
- // Final indices (indices into the final blended result vector of floats for the node)
- int idx = 0;
- for (BlendingMappingData &mapping : blendingMappingData) {
- switch (mapping.blendAction) {
- case BlendingMappingData::ClipBlending: {
- Q_ASSERT(mapping.channelIndicesClip1.size() == mapping.channelIndicesClip2.size());
- for (int i = 0, m = mapping.channelIndicesClip1.size(); i < m; ++i)
- mapping.channelIndices.push_back(idx++);
- break;
- }
- case BlendingMappingData::NoBlending: {
- const bool useClip1 = !mapping.channelIndicesClip1.empty();
- const QVector<int> channelIndices = useClip1 ? mapping.channelIndicesClip1 : mapping.channelIndicesClip2;
- for (int i = 0, m = channelIndices.size(); i < m; ++i) {
- mapping.channelIndices.push_back(idx++);
- }
- break;
- }
- default:
- Q_UNREACHABLE();
- break;
- }
- }
-
- return blendingMappingData;
-}
-
-void buildEntryForBlendClipNode(Handler *handler, const ChannelMapper *mapper, BlendedClipAnimator::BlendNodeData &nodeData)
-{
- // Retrieve Animation clips
- const AnimationClipLoader *clip1 = handler->animationClipLoaderManager()->lookupResource(nodeData.left);
- const AnimationClipLoader *clip2 = handler->animationClipLoaderManager()->lookupResource(nodeData.right);
-
- Q_ASSERT(clip1 && clip2);
-
- // Build mappings for the 2 clips
- const QVector<MappingData> mappingDataClip1 = buildPropertyMappings(handler, clip1, mapper);
- const QVector<MappingData> mappingDataClip2 = buildPropertyMappings(handler, clip2, mapper);
-
- // We can only blend channels that are in both clips
- // If a channel is present in one clip and not the other, we use 100% of its value (no blending)
- const QVector<BlendingMappingData> blendingMappingData = buildBlendMappingDataForNode(mappingDataClip1, mappingDataClip2);
- nodeData.mappingData = blendingMappingData;
-}
-
-void buildEntryForBlendNodeNode(BlendedClipAnimator::BlendNodeData &nodeData, const QHash<Qt3DCore::QNodeId, BlendedClipAnimator::BlendNodeData> &blendingNodeTable)
-{
- const BlendedClipAnimator::BlendNodeData &node1Data = blendingNodeTable.value(nodeData.left);
- const BlendedClipAnimator::BlendNodeData &node2Data = blendingNodeTable.value(nodeData.right);
-
- // Build mappings for the 2 nodes
- const QVector<BlendingMappingData> mappingDataNode1 = node1Data.mappingData;
- const QVector<BlendingMappingData> mappingDataNode2 = node2Data.mappingData;
-
- // We can only blend channels that are in both clips
- // If a channel is present in one clip and not the other, we use 100% of its value (no blending)
- const QVector<BlendingMappingData> blendingMappingData = buildBlendMappingDataForNode(mappingDataNode1, mappingDataNode2);
- nodeData.mappingData = blendingMappingData;
-}
-
-} // anonymous
-
// Note this job is run once for all stopped blended animators that have been marked dirty
// We assume that the structure of blend node tree does not change once a BlendClipAnimator has been set to running
void BuildBlendTreesJob::run()
@@ -180,42 +71,56 @@ void BuildBlendTreesJob::run()
const bool canRun = blendClipAnimator->canRun();
m_handler->setBlendedClipAnimatorRunning(blendedClipAnimatorHandle, canRun);
- // Check if blend clip can run and if so build blend tree
- if (canRun) {
- const ChannelMapper *mapper = m_handler->channelMapperManager()->lookupResource(blendClipAnimator->mapperId());
- Q_ASSERT(mapper);
-
- ClipBlendNodeVisitor visitor(m_handler->clipBlendNodeManager());
- QHash<Qt3DCore::QNodeId, BlendedClipAnimator::BlendNodeData> blendingNodeTable;
- // Build a Binary Tree of BlendNodeData
- Handler *handler = m_handler; // For lambda capture
- visitor.traverse(blendClipAnimator->blendTreeRootId(), [&] (ClipBlendNode *node) {
- BlendedClipAnimator::BlendNodeData nodeData;
- nodeData.blendNodeId = node->peerId();
- nodeData.type = (node->childrenIds().size() > 0)
- ? BlendedClipAnimator::BlendNodeData::BlendNodeType
- : BlendedClipAnimator::BlendNodeData::ClipType;
-
- if (nodeData.type == BlendedClipAnimator::BlendNodeData::BlendNodeType) {
- Q_ASSERT(node->childrenIds().size() == 2);
- nodeData.left = node->childrenIds().first();
- nodeData.right = node->childrenIds().last();
- buildEntryForBlendNodeNode(nodeData, blendingNodeTable);
- } else { // ClipType
- Q_ASSERT(node->clipIds().size() == 2);
- nodeData.left = node->clipIds().first();
- nodeData.right = node->clipIds().last();
- buildEntryForBlendClipNode(handler, mapper, nodeData);
- }
-
- // Note: we assume that ClipType are always leave nodes
- // We will therefore perform blending on all the ClipType nodes
- // Then traverse the tree of BlendNodeType and merge the values
- blendingNodeTable.insert(nodeData.blendNodeId, nodeData);
- });
+ if (!canRun)
+ continue;
+
+ // Build the format for clip results that should be used by nodes in the blend
+ // tree when used with this animator
+ const ChannelMapper *mapper = m_handler->channelMapperManager()->lookupResource(blendClipAnimator->mapperId());
+ Q_ASSERT(mapper);
+ QVector<ChannelNameAndType> channelNamesAndTypes
+ = buildRequiredChannelsAndTypes(m_handler, mapper);
+ QVector<ComponentIndices> channelComponentIndices
+ = assignChannelComponentIndices(channelNamesAndTypes);
+
+ // Find the leaf value nodes of the blend tree and for each of them
+ // create a set of format indices that can later be used to map the
+ // raw ClipResults resulting from evaluating an animation clip to the
+ // layout used by the blend tree for this animator
+ const QVector<Qt3DCore::QNodeId> valueNodeIds
+ = gatherValueNodesToEvaluate(m_handler, blendClipAnimator->blendTreeRootId());
+ for (const auto valueNodeId : valueNodeIds) {
+ ClipBlendValue *valueNode
+ = static_cast<ClipBlendValue *>(m_handler->clipBlendNodeManager()->lookupNode(valueNodeId));
+ Q_ASSERT(valueNode);
+
+ const Qt3DCore::QNodeId clipId = valueNode->clipId();
+ const AnimationClipLoader *clip = m_handler->animationClipLoaderManager()->lookupResource(clipId);
+ Q_ASSERT(clip);
+
+ const ComponentIndices formatIndices
+ = generateClipFormatIndices(channelNamesAndTypes,
+ channelComponentIndices,
+ clip);
+ valueNode->setFormatIndices(blendClipAnimator->peerId(), formatIndices);
+ }
- blendClipAnimator->setBlendTreeTable(blendingNodeTable);
+ // Finally, build the mapping data vector for this blended clip animator. This
+ // gets used during the final stage of evaluation when sending the property changes
+ // out to the targets of the animation. We do the costly work once up front.
+ const QVector<Qt3DCore::QNodeId> channelMappingIds = mapper->mappingIds();
+ QVector<ChannelMapping *> channelMappings;
+ channelMappings.reserve(channelMappingIds.size());
+ for (const auto mappingId : channelMappingIds) {
+ ChannelMapping *mapping = m_handler->channelMappingManager()->lookupResource(mappingId);
+ Q_ASSERT(mapping);
+ channelMappings.push_back(mapping);
}
+ const QVector<MappingData> mappingDataVec
+ = buildPropertyMappings(channelMappings,
+ channelNamesAndTypes,
+ channelComponentIndices);
+ blendClipAnimator->setMappingData(mappingDataVec);
}
}
diff --git a/src/animation/backend/evaluateblendclipanimatorjob.cpp b/src/animation/backend/evaluateblendclipanimatorjob.cpp
index a6ac5959f..08e6689ca 100644
--- a/src/animation/backend/evaluateblendclipanimatorjob.cpp
+++ b/src/animation/backend/evaluateblendclipanimatorjob.cpp
@@ -39,6 +39,7 @@
#include <Qt3DAnimation/private/managers_p.h>
#include <Qt3DAnimation/private/animationlogging_p.h>
#include <Qt3DAnimation/private/animationutils_p.h>
+#include <Qt3DAnimation/private/clipblendvalue_p.h>
#include <Qt3DAnimation/private/lerpclipblend_p.h>
#include <Qt3DAnimation/private/clipblendnodevisitor_p.h>
#include <Qt3DAnimation/private/job_common_p.h>
@@ -50,168 +51,69 @@ namespace Animation {
EvaluateBlendClipAnimatorJob::EvaluateBlendClipAnimatorJob()
: Qt3DCore::QAspectJob()
- , m_currentLoop(std::numeric_limits<int>::max())
- , m_isFinalFrame(true)
{
SET_JOB_RUN_STAT_TYPE(this, JobTypes::EvaluateBlendClipAnimator, 0);
}
-namespace {
-
-ClipResults blendValuesBasedOnMappings(ClipBlendNode *node,
- const ClipResults &channelResults1,
- const ClipResults &channelResults2,
- const QVector<BlendingMappingData> &blendingMappingData)
-{
- ClipResults blendedValues;
- blendedValues.reserve(blendingMappingData.size());
-
- // Build a combined vector of blended value
- for (const BlendingMappingData &mapping : blendingMappingData) {
-
- switch (mapping.blendAction) {
- case BlendingMappingData::ClipBlending: {
- Q_ASSERT(mapping.channelIndicesClip1.size() == mapping.channelIndicesClip2.size());
- for (int i = 0, m = mapping.channelIndicesClip1.size(); i < m; ++i) {
- const float value1 = channelResults1.at(mapping.channelIndicesClip1[i]);
- const float value2 = channelResults2.at(mapping.channelIndicesClip2[i]);
- const float blendedValue = node->blend(value1, value2);
- blendedValues.push_back(blendedValue);
- }
- break;
- }
- case BlendingMappingData::NoBlending: {
- const bool useClip1 = !mapping.channelIndicesClip1.empty();
- const QVector<int> channelIndices = useClip1 ? mapping.channelIndicesClip1 : mapping.channelIndicesClip2;
- const QVector<float> values = useClip1 ? channelResults1 : channelResults2;
- for (int i = 0, m = channelIndices.size(); i < m; ++i) {
- const float value = values.at(channelIndices[i]);
- blendedValues.push_back(value);
- }
- break;
- }
- default:
- Q_UNREACHABLE();
- break;
- }
- }
- return blendedValues;
-}
-
-QVector<MappingData> fromBlendingMappingData(const QVector<BlendingMappingData> &blendingMappingData)
-{
- const int blendingMappingDataSize = blendingMappingData.size();
- QVector<MappingData> mappingData(blendingMappingDataSize);
- for (int i = 0; i < blendingMappingDataSize; ++i) {
- mappingData[i] = blendingMappingData[i];
- }
- return mappingData;
-}
-
-} // anonymous
-
-void EvaluateBlendClipAnimatorJob::blendClips(ClipBlendNode *node,
- const BlendedClipAnimator::BlendNodeData &nodeData,
- const AnimatorEvaluationData &animatorEvaluationData)
-{
- AnimationClipLoader *clip1 = m_handler->animationClipLoaderManager()->lookupResource(nodeData.left);
- AnimationClipLoader *clip2 = m_handler->animationClipLoaderManager()->lookupResource(nodeData.right);
- Q_ASSERT(clip1 && clip2);
-
- // Prepare for evaluation (convert global time to local time ....)
- const ClipEvaluationData preEvaluationDataForClip1 = evaluationDataForClip(clip1, animatorEvaluationData);
- const ClipEvaluationData preEvaluationDataForClip2 = evaluationDataForClip(clip2, animatorEvaluationData);
-
- // Evaluate the fcurves for both clip
- const ClipResults channelResultsClip1 = evaluateClipAtLocalTime(clip1, preEvaluationDataForClip1.localTime);
- const ClipResults channelResultsClip2 = evaluateClipAtLocalTime(clip2, preEvaluationDataForClip2.localTime);
-
- // Update loops and running of the animator
- m_currentLoop = std::min(m_currentLoop, std::min(preEvaluationDataForClip1.currentLoop, preEvaluationDataForClip2.currentLoop));
- // isFinalFrame remains true only if all the clips have reached their final frame
- m_isFinalFrame &= (preEvaluationDataForClip1.isFinalFrame && preEvaluationDataForClip2.isFinalFrame);
-
- const QVector<BlendingMappingData> blendingMappingData = nodeData.mappingData;
- // Perform blending between the two clips
- const QVector<float> blendedValues = blendValuesBasedOnMappings(node, channelResultsClip1,
- channelResultsClip2, blendingMappingData);
-
- m_clipBlendResultsTable.insert(node, blendedValues);
-}
-
-void EvaluateBlendClipAnimatorJob::blendNodes(ClipBlendNode *node, const BlendedClipAnimator::BlendNodeData &nodeData)
-{
- ClipBlendNode *node1 = m_handler->clipBlendNodeManager()->lookupNode(nodeData.left);
- ClipBlendNode *node2 = m_handler->clipBlendNodeManager()->lookupNode(nodeData.right);
- Q_ASSERT(node1 && node2);
-
- // Retrieve results for the childNodes
- const ClipResults channelResultsNode1 = m_clipBlendResultsTable.take(node1);
- const ClipResults channelResultsNode2 = m_clipBlendResultsTable.take(node2);
-
- // Build a combined vector of blended value
- const QVector<BlendingMappingData> blendingMappingData = nodeData.mappingData;
- // Perform blending between the two nodes
- const QVector<float> blendedValues = blendValuesBasedOnMappings(node, channelResultsNode1,
- channelResultsNode2, blendingMappingData);
-
- m_clipBlendResultsTable.insert(node, blendedValues);
-}
-
void EvaluateBlendClipAnimatorJob::run()
{
- const qint64 globalTime = m_handler->simulationTime();
-
+ // Find the set of clips that need to be evaluated by querying each node
+ // in the blend tree.
+ // TODO: We should be able to cache this for each blend animator and only
+ // update when a node indicates its dependencies have changed as a result
+ // of blend factors changing
BlendedClipAnimator *blendedClipAnimator = m_handler->blendedClipAnimatorManager()->data(m_blendClipAnimatorHandle);
- Q_ASSERT(blendedClipAnimator);
-
- const AnimatorEvaluationData animatorEvaluationData = evaluationDataForAnimator(blendedClipAnimator, globalTime);
- const QHash<Qt3DCore::QNodeId, BlendedClipAnimator::BlendNodeData> blendindNodeTable = blendedClipAnimator->blendTreeTable();
-
- // Reset globals
- m_currentLoop = std::numeric_limits<int>::max();
- m_isFinalFrame = true;
+ Qt3DCore::QNodeId blendTreeRootId = blendedClipAnimator->blendTreeRootId();
+ const QVector<Qt3DCore::QNodeId> valueNodeIdsToEvaluate = gatherValueNodesToEvaluate(m_handler, blendTreeRootId);
- // Perform blending of the tree (Post-order traversal)
- ClipBlendNodeVisitor visitor(m_handler->clipBlendNodeManager());
- visitor.traverse(blendedClipAnimator->blendTreeRootId(), [&, this] (ClipBlendNode *blendNode) {
- // Retrieve BlendingData for the not
- const BlendedClipAnimator::BlendNodeData &nodeData = blendindNodeTable.value(blendNode->peerId());
+ // Calculate the resulting duration of the blend tree based upon its current state
+ ClipBlendNodeManager *blendNodeManager = m_handler->clipBlendNodeManager();
+ ClipBlendNode *blendTreeRootNode = blendNodeManager->lookupNode(blendTreeRootId);
+ Q_ASSERT(blendTreeRootNode);
+ const double duration = blendTreeRootNode->duration();
- switch (nodeData.type) {
- case BlendedClipAnimator::BlendNodeData::BlendNodeType:
- // Blend two ClipBlendNode
- blendNodes(blendNode, nodeData);
- break;
- case BlendedClipAnimator::BlendNodeData::ClipType: // Leaf
- // Blend two clips
- blendClips(blendNode, nodeData, animatorEvaluationData);
- break;
- default:
- Q_UNREACHABLE();
- break;
- }
- });
-
-
- // Set current loop and running if need
- if (m_isFinalFrame)
- blendedClipAnimator->setRunning(false);
- blendedClipAnimator->setCurrentLoop(m_currentLoop);
+ // Calculate the phase given the blend tree duration and global time
+ const qint64 globalTime = m_handler->simulationTime();
+ const AnimatorEvaluationData animatorData = evaluationDataForAnimator(blendedClipAnimator, globalTime);
+ int currentLoop = 0;
+ const double phase = phaseFromGlobalTime(animatorData.globalTime,
+ animatorData.startTime,
+ animatorData.playbackRate,
+ duration,
+ animatorData.loopCount,
+ currentLoop);
+
+ // Iterate over the value nodes of the blend tree, evaluate the
+ // contained animation clips at the current phase and store the results
+ // in the animator indexed by node.
+ // TODO: Handle clips not loaded from file
+ AnimationClipLoaderManager *clipLoaderManager = m_handler->animationClipLoaderManager();
+ for (const auto valueNodeId : valueNodeIdsToEvaluate) {
+ ClipBlendValue *valueNode = static_cast<ClipBlendValue *>(blendNodeManager->lookupNode(valueNodeId));
+ Q_ASSERT(valueNode);
+ AnimationClipLoader *clip = clipLoaderManager->lookupResource(valueNode->clipId());
+ Q_ASSERT(clip);
+
+ ClipResults rawClipResults = evaluateClipAtPhase(clip, phase);
+
+ // Reformat the clip results into the layout used by this animator/blend tree
+ ComponentIndices format = valueNode->formatIndices(blendedClipAnimator->peerId());
+ ClipResults formattedClipResults = formatClipResults(rawClipResults, format);
+ valueNode->setClipResults(blendedClipAnimator->peerId(), formattedClipResults);
+ }
- // Prepare property changes using the root node which should now be filled with correct values
- ClipBlendNode *rootBlendNode = m_handler->clipBlendNodeManager()->lookupNode(blendedClipAnimator->blendTreeRootId());
- Q_ASSERT(m_clipBlendResultsTable.size() == 1 && m_clipBlendResultsTable.contains(rootBlendNode));
+ // Evaluate the blend tree
+ ClipResults blendedResults = evaluateBlendTree(m_handler, blendedClipAnimator, blendTreeRootId);
- const ClipResults blendedValues = m_clipBlendResultsTable.take(rootBlendNode);
- const BlendedClipAnimator::BlendNodeData &rootNodeData = blendindNodeTable.value(rootBlendNode->peerId());
- const QVector<MappingData> mappingData = fromBlendingMappingData(rootNodeData.mappingData);
+ const double localTime = phase * duration;
+ const bool finalFrame = isFinalFrame(localTime, duration, currentLoop, animatorData.loopCount);
- // Prepare property changes (if finalFrame it also prepares the change for the running property for the frontend)
+ // Prepare the property change events
+ const QVector<MappingData> mappingData = blendedClipAnimator->mappingData();
const QVector<Qt3DCore::QSceneChangePtr> changes = preparePropertyChanges(blendedClipAnimator->peerId(),
- mappingData,
- blendedValues,
- m_isFinalFrame);
+ mappingData,
+ blendedResults,
+ finalFrame);
// Send the property changes
blendedClipAnimator->sendPropertyChanges(changes);
}
diff --git a/src/animation/backend/evaluateblendclipanimatorjob_p.h b/src/animation/backend/evaluateblendclipanimatorjob_p.h
index 60426d460..a7822c8f9 100644
--- a/src/animation/backend/evaluateblendclipanimatorjob_p.h
+++ b/src/animation/backend/evaluateblendclipanimatorjob_p.h
@@ -78,14 +78,6 @@ protected:
private:
HBlendedClipAnimator m_blendClipAnimatorHandle;
Handler *m_handler;
-
- void blendClips(ClipBlendNode *node, const BlendedClipAnimator::BlendNodeData &nodeData,
- const AnimatorEvaluationData &animatorEvaluationData);
- void blendNodes(ClipBlendNode *node, const BlendedClipAnimator::BlendNodeData &nodeData);
-
- QHash<ClipBlendNode *, QVector<float>> m_clipBlendResultsTable;
- int m_currentLoop;
- bool m_isFinalFrame;
};
typedef QSharedPointer<EvaluateBlendClipAnimatorJob> EvaluateBlendClipAnimatorJobPtr;