From f60b056b8a4b3a577fd0d0792c4796230684b50e Mon Sep 17 00:00:00 2001 From: Benjamin Jillich Date: Tue, 1 Feb 2022 13:45:33 +0100 Subject: [PATCH 1/5] Motion Matching: Added class description and member comments * Added description for Feature, FeatureMatrix, FeatureTrajectory classes. * Changed default residual type to absolute as that results in better visual quality. * Moved to //! doxygen style code to not affect /**/ sourounding temporary code commenting. Signed-off-by: Benjamin Jillich --- Gems/MotionMatching/Code/Source/Feature.h | 33 +++++++++++-------- .../Code/Source/FeatureMatrix.h | 14 +++++--- .../Code/Source/FeatureTrajectory.h | 10 +++--- Gems/MotionMatching/Code/Source/Frame.h | 14 ++++---- .../Code/Source/MotionMatchingData.h | 4 +-- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/Gems/MotionMatching/Code/Source/Feature.h b/Gems/MotionMatching/Code/Source/Feature.h index 9a0fe9fa8c..322cf03734 100644 --- a/Gems/MotionMatching/Code/Source/Feature.h +++ b/Gems/MotionMatching/Code/Source/Feature.h @@ -35,6 +35,15 @@ namespace EMotionFX::MotionMatching class MotionMatchingInstance; class TrajectoryQuery; + //! A feature is a property extracted from the animation data and is used by the motion matching algorithm to find the next best matching frame. + //! Examples of features are the position of the feet joints, the linear or angular velocity of the knee joints or the trajectory history and future + //! trajectory of the root joint. We can also encode environment sensations like obstacle positions and height, the location of the sword of an enemy + //! character or a football's position and velocity. Their purpose is to describe a frame of the animation by their key characteristics and sometimes + //! enhance the actual keyframe data (pos/rot/scale per joint) by e.g. taking the time domain into account and calculate the velocity or acceleration, + //! or a whole trajectory to describe where the given joint came from to reach the frame and the path it moves along in the near future. + //! @Note: Features are extracted and stored relative to a given joint, in most cases the motion extraction or root joint, and thus are in model-space. + //! This makes the search algorithm invariant to the character location and orientation and the extracted features, like e.g. a joint position or velocity, + //! translate and rotate along with the character. class EMFX_API Feature { public: @@ -89,8 +98,8 @@ namespace EMotionFX::MotionMatching }; virtual float CalculateFrameCost(size_t frameIndex, const FrameCostContext& context) const; - //! Specifies how the feature value differences (residuals), between the input query values - //! and the frames in the motion database that sum up the feature cost, are calculated. + /// Specifies how the feature value differences (residuals), between the input query values + /// and the frames in the motion database that sum up the feature cost, are calculated. enum ResidualType { Absolute, @@ -138,16 +147,14 @@ namespace EMotionFX::MotionMatching static void CalculateVelocity(const ActorInstance* actorInstance, size_t jointIndex, size_t relativeToJointIndex, const Frame& frame, AZ::Vector3& outVelocity); protected: - /** - * Calculate a normalized direction vector difference between the two given vectors. - * A dot product of the two vectors is taken and the result in range [-1, 1] is scaled to [0, 1]. - * @result Normalized, absolute difference between the vectors. - * Angle difference dot result cost - * 0.0 degrees 1.0 0.0 - * 90.0 degrees 0.0 0.5 - * 180.0 degrees -1.0 1.0 - * 270.0 degrees 0.0 0.5 - **/ + //! Calculate a normalized direction vector difference between the two given vectors. + //! A dot product of the two vectors is taken and the result in range [-1, 1] is scaled to [0, 1]. + //! @result Normalized, absolute difference between the vectors. + //! Angle difference dot result cost + //! 0.0 degrees 1.0 0.0 + //! 90.0 degrees 0.0 0.5 + //! 180.0 degrees -1.0 1.0 + //! 270.0 degrees 0.0 0.5 float GetNormalizedDirectionDifference(const AZ::Vector2& directionA, const AZ::Vector2& directionB) const; float GetNormalizedDirectionDifference(const AZ::Vector3& directionA, const AZ::Vector3& directionB) const; @@ -164,7 +171,7 @@ namespace EMotionFX::MotionMatching AZ::Color m_debugColor = AZ::Colors::Green; //< Color used for debug visualizations to identify the feature. bool m_debugDrawEnabled = false; //< Are debug visualizations enabled for this feature? float m_costFactor = 1.0f; //< The cost factor for the feature is multiplied with the actual and can be used to change a feature's influence in the motion matching search. - ResidualType m_residualType = ResidualType::Squared; //< How do we calculate the differences (residuals) between the input query values and the frames in the motion database that sum up the feature cost. + ResidualType m_residualType = ResidualType::Absolute; //< How do we calculate the differences (residuals) between the input query values and the frames in the motion database that sum up the feature cost. // Instance data (depends on the feature schema or actor instance). FeatureMatrix::Index m_featureColumnOffset; //< Float/Value offset, starting column for where the feature should be places at. diff --git a/Gems/MotionMatching/Code/Source/FeatureMatrix.h b/Gems/MotionMatching/Code/Source/FeatureMatrix.h index 1cf42933ce..645dcaf34a 100644 --- a/Gems/MotionMatching/Code/Source/FeatureMatrix.h +++ b/Gems/MotionMatching/Code/Source/FeatureMatrix.h @@ -15,7 +15,9 @@ #include #include -//#define O3DE_USE_EIGEN +//! Enable in case you want to use the Eigen SDK Eigen::Matrix as base for the feature matrix (https://eigen.tuxfamily.org/) +//! In case Eigen is disabled, a small simple NxM wrapper class is provided by default. +#define O3DE_USE_EIGEN #define O3DE_MM_FLOATTYPE float #ifdef O3DE_USE_EIGEN @@ -37,9 +39,7 @@ namespace EMotionFX::MotionMatching // RowMajor: Store row components next to each other in memory for cache-optimized feature access for a given frame. using FeatureMatrixType = Eigen::Matrix; #else - /** - * Small wrapper for a 2D matrix similar to the Eigen::Matrix. - */ + //! Small wrapper for a 2D matrix similar to the Eigen::Matrix. class FeatureMatrixType { public: @@ -87,6 +87,12 @@ namespace EMotionFX::MotionMatching }; #endif + //! The feature matrix is a NxM matrix which stores the extracted feature values for all frames in our motion database based upon a given feature schema. + //! The feature schema defines the order of the columns and values and is used to identify values and find their location inside the matrix. + //! A 3D position feature storing XYZ values e.g. will use three columns in the feature matrix. Every component of a feature is linked to a column index, + //! so e.g. the left foot position Y value might be at column index 6. The group of values or columns that belong to a given feature is what we call a feature block. + //! The accumulated number of dimensions for all features in the schema, while the number of dimensions might vary per feature, form the number of columns of the feature matrix. + //! Each row represents the features of a single frame of the motion database. The number of rows of the feature matrix is defined by the number. class FeatureMatrix : public FeatureMatrixType { diff --git a/Gems/MotionMatching/Code/Source/FeatureTrajectory.h b/Gems/MotionMatching/Code/Source/FeatureTrajectory.h index 7eacbf684c..af4d933933 100644 --- a/Gems/MotionMatching/Code/Source/FeatureTrajectory.h +++ b/Gems/MotionMatching/Code/Source/FeatureTrajectory.h @@ -29,12 +29,10 @@ namespace EMotionFX::MotionMatching { class FrameDatabase; - /** - * Matches the root joint past and future trajectory. - * For each frame in the motion database, the position and facing direction relative to the current frame of the joint will be evaluated for a past and future time window. - * The past and future samples together form the trajectory of the current frame within the time window. This basically describes where the character came from to reach the - * current frame and where it will go when continuing to play the animation. - **/ + //! Matches the root joint past and future trajectory. + //! For each frame in the motion database, the position and facing direction relative to the current frame of the joint will be evaluated for a past and future time window. + //! The past and future samples together form the trajectory of the current frame within the time window. This basically describes where the character came from to reach the + //! current frame and where it will go when continuing to play the animation. class EMFX_API FeatureTrajectory : public Feature { diff --git a/Gems/MotionMatching/Code/Source/Frame.h b/Gems/MotionMatching/Code/Source/Frame.h index 02150cfa5a..b362cc0b3e 100644 --- a/Gems/MotionMatching/Code/Source/Frame.h +++ b/Gems/MotionMatching/Code/Source/Frame.h @@ -20,10 +20,8 @@ namespace EMotionFX namespace MotionMatching { - /** - * A motion matching frame. - * This holds information required in order to extract a given pose in a given motion. - */ + //! A motion matching frame. + //! This holds information required in order to extract a given pose in a given motion. class EMFX_API Frame { public: @@ -53,10 +51,10 @@ namespace EMotionFX void SetMirrored(bool enabled); private: - size_t m_frameIndex = 0; /**< The motion frame index inside the data object. */ - float m_sampleTime = 0.0f; /**< The time offset in the original motion. */ - Motion* m_sourceMotion = nullptr; /**< The original motion that we sample from to restore the pose. */ - bool m_mirrored = false; /**< Is this frame mirrored? */ + size_t m_frameIndex = 0; //< The motion frame index inside the data object. + float m_sampleTime = 0.0f; //< The time offset in the original motion. + Motion* m_sourceMotion = nullptr; //< The original motion that we sample from to restore the pose. + bool m_mirrored = false; //< Is this frame mirrored? }; } // namespace MotionMatching } // namespace EMotionFX diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingData.h b/Gems/MotionMatching/Code/Source/MotionMatchingData.h index 15748bb849..4e02d1a468 100644 --- a/Gems/MotionMatching/Code/Source/MotionMatchingData.h +++ b/Gems/MotionMatching/Code/Source/MotionMatchingData.h @@ -63,12 +63,12 @@ namespace EMotionFX::MotionMatching protected: bool ExtractFeatures(ActorInstance* actorInstance, FrameDatabase* frameDatabase, size_t maxKdTreeDepth=20, size_t minFramesPerKdTreeNode=2000); - FrameDatabase m_frameDatabase; /**< The animation database with all the keyframes and joint transform data. */ + FrameDatabase m_frameDatabase; //< The animation database with all the keyframes and joint transform data. const FeatureSchema& m_featureSchema; FeatureMatrix m_featureMatrix; - AZStd::unique_ptr m_kdTree; /**< The acceleration structure to speed up the search for lowest cost frames. */ + AZStd::unique_ptr m_kdTree; //< The acceleration structure to speed up the search for lowest cost frames. AZStd::vector m_featuresInKdTree; }; } // namespace EMotionFX::MotionMatching From a01500eaaad274b4195165b0012a44f50999cc86 Mon Sep 17 00:00:00 2001 From: Benjamin Jillich Date: Tue, 1 Feb 2022 13:48:03 +0100 Subject: [PATCH 2/5] Motion Matching: Automatic target mode for trajectory query * Consolidated the different automatic path generation modes into a single automatic demo mode. * Moved the static phase variable to a member so that multiple instances won't follow the same path and speedup the phase increment. Signed-off-by: Benjamin Jillich --- .../Code/Source/TrajectoryQuery.cpp | 52 ++++--------------- .../Code/Source/TrajectoryQuery.h | 7 ++- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp b/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp index 803623a1af..53f6f80ebf 100644 --- a/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp +++ b/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp @@ -12,44 +12,13 @@ namespace EMotionFX::MotionMatching { - AZ::Vector3 SampleFunction(TrajectoryQuery::EMode mode, float offset, float radius, float phase) + AZ::Vector3 SampleFunction(float offset, float radius, float phase) { - switch (mode) - { - case TrajectoryQuery::MODE_TWO: - { - AZ::Vector3 displacement = AZ::Vector3::CreateZero(); - displacement.SetX(radius * sinf(phase + offset) ); - displacement.SetY(cosf(phase + offset)); - return displacement; - } - - case TrajectoryQuery::MODE_THREE: - { - AZ::Vector3 displacement = AZ::Vector3::CreateZero(); - const float rad = radius * cosf(radius + phase*0.2f); - displacement.SetX(rad * sinf(phase + offset)); - displacement.SetY(rad * cosf(phase + offset)); - return displacement; - } - - case TrajectoryQuery::MODE_FOUR: - { - AZ::Vector3 displacement = AZ::Vector3::CreateZero(); - displacement.SetX(radius * sinf(phase + offset)); - displacement.SetY(radius*2.0f * cosf(phase + offset)); - return displacement; - } - - // MODE_ONE and default - default: - { - AZ::Vector3 displacement = AZ::Vector3::CreateZero(); - displacement.SetX(radius * sinf(phase * 0.7f + offset) + radius * 0.75f * cosf(phase * 2.0f + offset * 2.0f)); - displacement.SetY(radius * cosf(phase * 0.4f + offset)); - return displacement; - } - } + phase += 10.7; + AZ::Vector3 displacement = AZ::Vector3::CreateZero(); + displacement.SetX(radius * sinf(phase * 0.7f + offset) + radius * 0.75f * cosf(phase * 2.0f + offset * 2.0f)); + displacement.SetY(radius * cosf(phase * 0.4f + offset)); + return displacement; } void TrajectoryQuery::Update(const ActorInstance* actorInstance, @@ -88,19 +57,18 @@ namespace EMotionFX::MotionMatching } else { - static float phase = 0.0f; - phase += timeDelta * pathSpeed; - AZ::Vector3 base = SampleFunction(mode, 0.0f, pathRadius, phase); + m_automaticModePhase += timeDelta * pathSpeed; + AZ::Vector3 base = SampleFunction(0.0f, pathRadius, m_automaticModePhase); for (size_t i = 0; i < numFutureSamples; ++i) { const float offset = i * 0.1f; - const AZ::Vector3 curSample = SampleFunction(mode, offset, pathRadius, phase); + const AZ::Vector3 curSample = SampleFunction(offset, pathRadius, m_automaticModePhase); AZ::Vector3 displacement = curSample - base; m_futureControlPoints[i].m_position = actorInstance->GetWorldSpaceTransform().m_position + displacement; // Evaluate a control point slightly further into the future than the actual // one and use the position difference as the facing direction. - const AZ::Vector3 deltaSample = SampleFunction(mode, offset + 0.01f, pathRadius, phase); + const AZ::Vector3 deltaSample = SampleFunction(offset + 0.01f, pathRadius, m_automaticModePhase); const AZ::Vector3 dir = deltaSample - curSample; m_futureControlPoints[i].m_facingDirection = dir.GetNormalizedSafe(); } diff --git a/Gems/MotionMatching/Code/Source/TrajectoryQuery.h b/Gems/MotionMatching/Code/Source/TrajectoryQuery.h index 55d9ecf797..0793ef03f6 100644 --- a/Gems/MotionMatching/Code/Source/TrajectoryQuery.h +++ b/Gems/MotionMatching/Code/Source/TrajectoryQuery.h @@ -36,10 +36,7 @@ namespace EMotionFX::MotionMatching enum EMode : AZ::u8 { MODE_TARGETDRIVEN = 0, - MODE_ONE = 1, - MODE_TWO = 2, - MODE_THREE = 3, - MODE_FOUR = 4 + MODE_AUTOMATIC = 1 }; void Update(const ActorInstance* actorInstance, @@ -64,5 +61,7 @@ namespace EMotionFX::MotionMatching AZStd::vector m_pastControlPoints; AZStd::vector m_futureControlPoints; + + float m_automaticModePhase = 0.0f; //< Current phase for the automatic demo mode. Not needed by the target-driven mode. }; } // namespace EMotionFX::MotionMatching From 83869d706415dd22544124605b17fee81f04c63f Mon Sep 17 00:00:00 2001 From: Benjamin Jillich Date: Tue, 1 Feb 2022 13:49:02 +0100 Subject: [PATCH 3/5] Motion Matching: Fixed crash on empty KD-tree Signed-off-by: Benjamin Jillich --- Gems/MotionMatching/Code/Source/FeatureMatrix.h | 2 +- Gems/MotionMatching/Code/Source/KdTree.cpp | 16 ++++++++++++---- Gems/MotionMatching/Code/Source/KdTree.h | 10 ++++------ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Gems/MotionMatching/Code/Source/FeatureMatrix.h b/Gems/MotionMatching/Code/Source/FeatureMatrix.h index 645dcaf34a..4f1f69a5e5 100644 --- a/Gems/MotionMatching/Code/Source/FeatureMatrix.h +++ b/Gems/MotionMatching/Code/Source/FeatureMatrix.h @@ -17,7 +17,7 @@ //! Enable in case you want to use the Eigen SDK Eigen::Matrix as base for the feature matrix (https://eigen.tuxfamily.org/) //! In case Eigen is disabled, a small simple NxM wrapper class is provided by default. -#define O3DE_USE_EIGEN +//#define O3DE_USE_EIGEN #define O3DE_MM_FLOATTYPE float #ifdef O3DE_USE_EIGEN diff --git a/Gems/MotionMatching/Code/Source/KdTree.cpp b/Gems/MotionMatching/Code/Source/KdTree.cpp index e496f0b63e..e2de897946 100644 --- a/Gems/MotionMatching/Code/Source/KdTree.cpp +++ b/Gems/MotionMatching/Code/Source/KdTree.cpp @@ -48,11 +48,13 @@ namespace EMotionFX::MotionMatching Clear(); // Verify the dimensions. - // Going above a 20 dimensional tree would start eating up too much memory. m_numDimensions = CalcNumDimensions(features); - if (m_numDimensions == 0 || m_numDimensions > 20) + + // Going above a 48 dimensional tree would start eating up too much memory. + const size_t maxNumDimensions = 48; + if (m_numDimensions == 0 || m_numDimensions > maxNumDimensions) { - AZ_Error("Motion Matching", false, "Cannot initialize KD-tree. KD-tree dimension (%d) has to be between 1 and 20. Please use Feature::SetIncludeInKdTree(false) on some features.", m_numDimensions); + AZ_Error("Motion Matching", false, "Cannot initialize KD-tree. KD-tree dimension (%d) has to be between 1 and %zu. Please use Feature::SetIncludeInKdTree(false) on some features.", m_numDimensions, maxNumDimensions); return false; } @@ -203,6 +205,12 @@ namespace EMotionFX::MotionMatching void KdTree::MergeSmallLeafNodesToParents() { + // If the tree is empty or only has a single node, there is nothing to merge. + if (m_nodes.size() < 2) + { + return; + } + AZStd::vector nodesToRemove; for (Node* node : m_nodes) { @@ -410,7 +418,7 @@ namespace EMotionFX::MotionMatching } else { - // We have both a left and right node, so we're not at a leaf yet. + // We have either a left and right node, so we're not at a leaf yet. if (curNode->m_leftNode) { if (frameFloats[d] <= curNode->m_median) diff --git a/Gems/MotionMatching/Code/Source/KdTree.h b/Gems/MotionMatching/Code/Source/KdTree.h index 8b62788c32..70b8652645 100644 --- a/Gems/MotionMatching/Code/Source/KdTree.h +++ b/Gems/MotionMatching/Code/Source/KdTree.h @@ -34,12 +34,10 @@ namespace EMotionFX::MotionMatching size_t maxDepth=10, size_t minFramesPerLeaf=1000); - /** - * Calculate the number of dimensions or values for the given feature set. - * Each feature might store one or multiple values inside the feature matrix and the number of - * values each feature holds varies with the feature type. This calculates the sum of the number of - * values of the given feature set. - */ + //! Calculate the number of dimensions or values for the given feature set. + //! Each feature might store one or multiple values inside the feature matrix and the number of + //! values each feature holds varies with the feature type. This calculates the sum of the number of + //! values of the given feature set. static size_t CalcNumDimensions(const AZStd::vector& features); void Clear(); From 3eb661c4bcb45dd945b2333741c6bf872f82f37b Mon Sep 17 00:00:00 2001 From: Benjamin Jillich Date: Tue, 1 Feb 2022 13:53:13 +0100 Subject: [PATCH 4/5] Motion Matching: Moved discard event check into consolidated IsFrameDiscarded() method * Moved the extract events call and the check for a discard frame event to the IsFrameDiscarded() function. * Added class description for the frame database and ported the comments to //! and //< Signed-off-by: Benjamin Jillich --- .../Code/Source/FrameDatabase.cpp | 22 +++++------ .../Code/Source/FrameDatabase.h | 39 +++++++++++-------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Gems/MotionMatching/Code/Source/FrameDatabase.cpp b/Gems/MotionMatching/Code/Source/FrameDatabase.cpp index 7d38060dcc..307a45a974 100644 --- a/Gems/MotionMatching/Code/Source/FrameDatabase.cpp +++ b/Gems/MotionMatching/Code/Source/FrameDatabase.cpp @@ -53,7 +53,7 @@ namespace EMotionFX::MotionMatching m_usedMotions.shrink_to_fit(); } - void FrameDatabase::ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector& activeEventDatas) + void FrameDatabase::ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector& activeEventDatas) const { activeEventDatas.clear(); @@ -84,9 +84,11 @@ namespace EMotionFX::MotionMatching } } - bool FrameDatabase::IsFrameDiscarded(const AZStd::vector& activeEventDatas) const + bool FrameDatabase::IsFrameDiscarded(const Motion* motion, float frameTime, AZStd::vector& activeEvents) const { - for (const EventData* eventData : activeEventDatas) + // Is frame discarded by a motion event? + ExtractActiveMotionEventDatas(motion, frameTime, activeEvents); + for (const EventData* eventData : activeEvents) { if (eventData->RTTI_GetType() == azrtti_typeid()) { @@ -126,11 +128,10 @@ namespace EMotionFX::MotionMatching double curTime = 0.0; while (curTime <= totalTime) { - const float floatTime = aznumeric_cast(curTime); - ExtractActiveMotionEventDatas(motion, floatTime, activeEvents); - if (!IsFrameDiscarded(activeEvents)) + const float frameTime = aznumeric_cast(curTime); + if (!IsFrameDiscarded(motion, frameTime, activeEvents)) { - ImportFrame(motion, floatTime, mirrored); + ImportFrame(motion, frameTime, mirrored); numFramesImported++; } else @@ -143,11 +144,10 @@ namespace EMotionFX::MotionMatching // Make sure we include the last frame, if we stepped over it. if (curTime - timeStep < totalTime - 0.000001) { - const float floatTime = aznumeric_cast(totalTime); - ExtractActiveMotionEventDatas(motion, floatTime, activeEvents); - if (!IsFrameDiscarded(activeEvents)) + const float frameTime = aznumeric_cast(totalTime); + if (!IsFrameDiscarded(motion, frameTime, activeEvents)) { - ImportFrame(motion, floatTime, mirrored); + ImportFrame(motion, frameTime, mirrored); numFramesImported++; } else diff --git a/Gems/MotionMatching/Code/Source/FrameDatabase.h b/Gems/MotionMatching/Code/Source/FrameDatabase.h index c5258e1b39..45afe98825 100644 --- a/Gems/MotionMatching/Code/Source/FrameDatabase.h +++ b/Gems/MotionMatching/Code/Source/FrameDatabase.h @@ -29,29 +29,36 @@ namespace EMotionFX::MotionMatching class MotionMatchingInstance; class MotionMatchEventData; - // The motion matching data. - // This is basically a database of frames (which point to motion objects), together with meta data per frame. - // No actual pose data is stored directly inside this class, just references to the right sample times inside specific motions. + //! A set of frames from your animations sampled at a given sample rate is stored in the frame database. A frame object knows about its index in the frame database, + //! the animation it belongs to and the sample time in seconds. It does not hold the actual sampled pose for memory reasons as the `EMotionFX::Motion` already store the + //! transform keyframes. + //! The sample rate of the animation might differ from the sample rate used for the frame database. For example, your animations might be recorded with 60 Hz while we only want + //! to extract the features with a sample rate of 30 Hz. As the motion matching algorithm is blending between the frames in the motion database while playing the animation window + //! between the jumps/blends, it can make sense to have animations with a higher sample rate than we use to extract the features. + //! A frame of the motion database can be used to sample a pose from which we can extract the features. It also provides functionality to sample a pose with a time offset to that frame. + //! This can be handy in order to calculate joint velocities or trajectory samples. + //! When importing animations, frames that are within the range of a discard frame motion event are ignored and won't be added to the motion database. Discard motion events can be + //! used to cut out sections of the imported animations that are unwanted like a stretching part between two dance cards. class EMFX_API FrameDatabase { public: AZ_RTTI(FrameDatabase, "{3E5ED4F9-8975-41F2-B665-0086368F0DDA}") AZ_CLASS_ALLOCATOR_DECL - // The settings used when importing motions into the frame database. - // Used in combination with ImportFrames(). + //! The settings used when importing motions into the frame database. + //! Used in combination with ImportFrames(). struct EMFX_API FrameImportSettings { - size_t m_sampleRate = 30; /**< Sample at 30 frames per second on default. */ - bool m_autoShrink = true; /**< Automatically shrink the internal frame arrays to their minimum size afterwards. */ + size_t m_sampleRate = 30; //< Sample at 30 frames per second on default. + bool m_autoShrink = true; //< Automatically shrink the internal frame arrays to their minimum size afterwards. }; FrameDatabase(); virtual ~FrameDatabase(); // Main functions. - AZStd::tuple ImportFrames(Motion* motion, const FrameImportSettings& settings, bool mirrored); // Returns the number of imported frames and the number of discarded frames as second element. - void Clear(); // Clear the data, so you can re-initialize it with new data. + AZStd::tuple ImportFrames(Motion* motion, const FrameImportSettings& settings, bool mirrored); //< Returns the number of imported frames and the number of discarded frames as second element. + void Clear(); //< Clear the data, so you can re-initialize it with new data. // Statistics. size_t GetNumFrames() const; @@ -66,21 +73,19 @@ namespace EMotionFX::MotionMatching const AZStd::vector& GetUsedMotions() const; size_t GetSampleRate() const { return m_sampleRate; } - /** - * Find the frame index for the given playtime and motion. - * NOTE: This is a slow operation and should not be used by the runtime without visual debugging. - */ + //! Find the frame index for the given playtime and motion. + //! NOTE: This is a slow operation and should not be used by the runtime without visual debugging. size_t FindFrameIndex(Motion* motion, float playtime) const; private: void ImportFrame(Motion* motion, float timeValue, bool mirrored); - bool IsFrameDiscarded(const AZStd::vector& activeEventDatas) const; - void ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector& activeEventDatas); // Vector will be cleared internally. + bool IsFrameDiscarded(const Motion* motion, float frameTime, AZStd::vector& activeEvents) const; + void ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector& activeEventDatas) const; // Vector will be cleared internally. private: - AZStd::vector m_frames; /**< The collection of frames. Keep in mind these don't hold a pose, but reference to a given frame/time value inside a given motion. */ + AZStd::vector m_frames; //< The collection of frames. Keep in mind these don't hold a pose, but reference to a given frame/time value inside a given motion. AZStd::unordered_map> m_frameIndexByMotion; - AZStd::vector m_usedMotions; /**< The list of used motions. */ + AZStd::vector m_usedMotions; //< The list of used motions. size_t m_sampleRate = 0; }; } // namespace EMotionFX::MotionMatching From 0b63fed9c9790e39a8babd7d90555f791da6e69e Mon Sep 17 00:00:00 2001 From: Benjamin Jillich Date: Wed, 2 Feb 2022 10:38:58 +0100 Subject: [PATCH 5/5] Motion Matching: Fixed several bugs and improved anim graph node RPE layout * Fixed several crashes that happened when adjusting the cost function in the UI. * Automatically hide the trajectory path radius and path speed properties when the mode is set to target-based. * Re-ordered the properties for better grouping. Signed-off-by: Benjamin Jillich --- .../Code/Source/BlendTreeMotionMatchNode.cpp | 67 +++++++++++-------- .../Code/Source/BlendTreeMotionMatchNode.h | 2 + 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp index 58cd3903e1..6661233b66 100644 --- a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp +++ b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp @@ -191,7 +191,11 @@ namespace EMotionFX::MotionMatching // set the current time to the new calculated time uniqueData->ClearInheritFlags(); - uniqueData->SetPreSyncTime(instance->GetMotionInstance()->GetCurrentTime()); + + if (instance->GetMotionInstance()) + { + uniqueData->SetPreSyncTime(instance->GetMotionInstance()->GetCurrentTime()); + } uniqueData->SetCurrentPlayTime(instance->GetNewMotionTime()); if (uniqueData->GetPreSyncTime() > uniqueData->GetCurrentPlayTime()) @@ -230,9 +234,12 @@ namespace EMotionFX::MotionMatching } MotionInstance* motionInstance = instance->GetMotionInstance(); - motionInstance->UpdateByTimeValues(uniqueData->GetPreSyncTime(), uniqueData->GetCurrentPlayTime(), &data->GetEventBuffer()); + if (motionInstance) + { + motionInstance->UpdateByTimeValues(uniqueData->GetPreSyncTime(), uniqueData->GetCurrentPlayTime(), &data->GetEventBuffer()); + uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); + } - uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); data->GetEventBuffer().UpdateEmitters(this); instance->PostUpdate(timePassedInSeconds); @@ -240,7 +247,6 @@ namespace EMotionFX::MotionMatching const Transform& trajectoryDelta = instance->GetMotionExtractionDelta(); data->SetTrajectoryDelta(trajectoryDelta); data->SetTrajectoryDeltaMirrored(trajectoryDelta); // TODO: use a real mirrored version here. - m_postUpdateTimeInMs = m_timer.GetDeltaTimeInSeconds() * 1000.0f; } @@ -251,12 +257,10 @@ namespace EMotionFX::MotionMatching AZ_UNUSED(animGraphInstance); m_timer.Stamp(); - AnimGraphPose* outputPose; - // Initialize to bind pose. ActorInstance* actorInstance = animGraphInstance->GetActorInstance(); RequestPoses(animGraphInstance); - outputPose = GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue(); + AnimGraphPose* outputPose = GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue(); outputPose->InitFromBindPose(actorInstance); if (m_disabled) @@ -287,7 +291,6 @@ namespace EMotionFX::MotionMatching // Performance metrics m_outputTimeInMs = m_timer.GetDeltaTimeInSeconds() * 1000.0f; { - //AZ_Printf("MotionMatch", "Update = %.2f, PostUpdate = %.2f, Output = %.2f", m_updateTime, m_postUpdateTime, m_outputTime); #ifdef IMGUI_ENABLED ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "Update", m_updateTimeInMs); ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "Post Update", m_postUpdateTimeInMs); @@ -298,6 +301,16 @@ namespace EMotionFX::MotionMatching instance->DebugDraw(); } + AZ::Crc32 BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility() const + { + if (m_trajectoryQueryMode == TrajectoryQuery::MODE_TARGETDRIVEN) + { + return AZ::Edit::PropertyVisibility::Hide; + } + + return AZ::Edit::PropertyVisibility::Show; + } + void BlendTreeMotionMatchNode::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); @@ -339,32 +352,32 @@ namespace EMotionFX::MotionMatching ->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits::max()) ->Attribute(AZ::Edit::Attributes::Step, 0.05f) ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_maxKdTreeDepth, "Max kdTree depth", "The maximum number of hierarchy levels in the kdTree.") - ->Attribute(AZ::Edit::Attributes::Min, 1) - ->Attribute(AZ::Edit::Attributes::Max, 20) - ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute(AZ::Edit::Attributes::Max, 20) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_minFramesPerKdTreeNode, "Min kdTree node size", "The minimum number of frames to store per kdTree node.") - ->Attribute(AZ::Edit::Attributes::Min, 1) - ->Attribute(AZ::Edit::Attributes::Max, 100000) - ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute(AZ::Edit::Attributes::Max, 100000) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) + ->DataElement(AZ::Edit::UIHandlers::ComboBox, &BlendTreeMotionMatchNode::m_trajectoryQueryMode, "Trajectory Prediction", "Desired future trajectory generation mode.") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) + ->EnumAttribute(TrajectoryQuery::MODE_TARGETDRIVEN, "Target-driven") + ->EnumAttribute(TrajectoryQuery::MODE_AUTOMATIC, "Automatic (Demo)") ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_pathRadius, "Path radius", "") - ->Attribute(AZ::Edit::Attributes::Min, 0.0001f) - ->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits::max()) - ->Attribute(AZ::Edit::Attributes::Step, 0.01f) + ->Attribute(AZ::Edit::Attributes::Visibility, &BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility) + ->Attribute(AZ::Edit::Attributes::Min, 0.0001f) + ->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits::max()) + ->Attribute(AZ::Edit::Attributes::Step, 0.01f) ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_pathSpeed, "Path speed", "") - ->Attribute(AZ::Edit::Attributes::Min, 0.0001f) - ->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits::max()) - ->Attribute(AZ::Edit::Attributes::Step, 0.01f) - ->DataElement(AZ::Edit::UIHandlers::ComboBox, &BlendTreeMotionMatchNode::m_trajectoryQueryMode, "Trajectory mode", "Desired future trajectory generation mode.") - ->EnumAttribute(TrajectoryQuery::MODE_TARGETDRIVEN, "Target driven") - ->EnumAttribute(TrajectoryQuery::MODE_ONE, "Mode one") - ->EnumAttribute(TrajectoryQuery::MODE_TWO, "Mode two") - ->EnumAttribute(TrajectoryQuery::MODE_THREE, "Mode three") - ->EnumAttribute(TrajectoryQuery::MODE_FOUR, "Mode four") + ->Attribute(AZ::Edit::Attributes::Visibility, &BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility) + ->Attribute(AZ::Edit::Attributes::Min, 0.0001f) + ->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits::max()) + ->Attribute(AZ::Edit::Attributes::Step, 0.01f) ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_featureSchema, "FeatureSchema", "") ->Attribute(AZ::Edit::Attributes::AutoExpand, "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) - ->DataElement(AZ_CRC("MotionSetMotionIds", 0x8695c0fa), &BlendTreeMotionMatchNode::m_motionIds, "Motions", "") + ->DataElement(AZ_CRC_CE("MotionSetMotionIds"), &BlendTreeMotionMatchNode::m_motionIds, "Motions", "") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) ->Attribute(AZ::Edit::Attributes::ContainerCanBeModified, false) ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::HideChildren) diff --git a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h index 3acae96d2c..554a80b31a 100644 --- a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h +++ b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h @@ -87,6 +87,8 @@ namespace EMotionFX::MotionMatching void Update(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override; void PostUpdate(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override; + AZ::Crc32 GetTrajectoryPathSettingsVisibility() const; + FeatureSchema m_featureSchema; AZStd::vector m_motionIds;