diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_FileMenuOptions.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_FileMenuOptions.py index 0060a3b29a..bedf474387 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_FileMenuOptions.py +++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_FileMenuOptions.py @@ -38,7 +38,7 @@ def Menus_FileMenuOptions_Work(): ("Save As",), ("Save Level Statistics",), ("Edit Project Settings",), - #("Edit Platform Settings",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6604 + ("Edit Platform Settings",), ("New Project",), ("Open Project",), ("Show Log File",), diff --git a/Code/Editor/Core/QtEditorApplication.h b/Code/Editor/Core/QtEditorApplication.h index 0d702bf647..28ee8ac14b 100644 --- a/Code/Editor/Core/QtEditorApplication.h +++ b/Code/Editor/Core/QtEditorApplication.h @@ -13,7 +13,6 @@ #include #include #include -#include "IEventLoopHook.h" #include #include diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 982f8ba411..480a973282 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -1828,34 +1828,6 @@ bool CCryEditApp::InitInstance() return true; } -void CCryEditApp::RegisterEventLoopHook(IEventLoopHook* pHook) -{ - pHook->pNextHook = m_pEventLoopHook; - m_pEventLoopHook = pHook; -} - -void CCryEditApp::UnregisterEventLoopHook(IEventLoopHook* pHookToRemove) -{ - IEventLoopHook* pPrevious = nullptr; - for (IEventLoopHook* pHook = m_pEventLoopHook; pHook != nullptr; pHook = pHook->pNextHook) - { - if (pHook == pHookToRemove) - { - if (pPrevious) - { - pPrevious->pNextHook = pHookToRemove->pNextHook; - } - else - { - m_pEventLoopHook = pHookToRemove->pNextHook; - } - - pHookToRemove->pNextHook = nullptr; - return; - } - } -} - ////////////////////////////////////////////////////////////////////////// void CCryEditApp::LoadFile(QString fileName) { diff --git a/Code/Editor/CryEdit.h b/Code/Editor/CryEdit.h index 48f362003c..f472639dcd 100644 --- a/Code/Editor/CryEdit.h +++ b/Code/Editor/CryEdit.h @@ -30,7 +30,6 @@ class CConsoleDialog; struct mg_connection; struct mg_request_info; struct mg_context; -struct IEventLoopHook; class QAction; class MainWindow; class QSharedMemory; @@ -153,8 +152,6 @@ public: int IdleProcessing(bool bBackground); bool IsWindowInForeground(); void RunInitPythonScript(CEditCommandLineInfo& cmdInfo); - void RegisterEventLoopHook(IEventLoopHook* pHook); - void UnregisterEventLoopHook(IEventLoopHook* pHook); void DisableIdleProcessing() override; void EnableIdleProcessing() override; @@ -344,7 +341,6 @@ private: QString m_lastOpenLevelPath; CQuickAccessBar* m_pQuickAccessBar = nullptr; - IEventLoopHook* m_pEventLoopHook = nullptr; QString m_rootEnginePath; int m_disableIdleProcessingCounter = 0; //!< Counts requests to disable idle processing. When non-zero, idle processing will be disabled. diff --git a/Code/Editor/IEditor.h b/Code/Editor/IEditor.h index da29b00f2d..0de08445b4 100644 --- a/Code/Editor/IEditor.h +++ b/Code/Editor/IEditor.h @@ -66,7 +66,6 @@ class IAWSResourceManager; struct ISystem; struct IRenderer; struct AABB; -struct IEventLoopHook; struct IErrorReport; // Vladimir@conffx struct IFileUtil; // Vladimir@conffx struct IEditorLog; // Vladimir@conffx @@ -509,11 +508,6 @@ struct IEditor virtual void SetActiveView(CViewport* viewport) = 0; virtual struct IEditorFileMonitor* GetFileMonitor() = 0; - // These are needed for Qt integration: - virtual void RegisterEventLoopHook(IEventLoopHook* pHook) = 0; - virtual void UnregisterEventLoopHook(IEventLoopHook* pHook) = 0; - // ^^^ - //! QMimeData is used by the Qt clipboard. //! IMPORTANT: Any QMimeData allocated for the clipboard will be deleted //! when the editor exists. If a QMimeData is allocated by a different diff --git a/Code/Editor/IEditorImpl.cpp b/Code/Editor/IEditorImpl.cpp index 05b74f3b05..b6a4209948 100644 --- a/Code/Editor/IEditorImpl.cpp +++ b/Code/Editor/IEditorImpl.cpp @@ -789,16 +789,6 @@ IEditorFileMonitor* CEditorImpl::GetFileMonitor() return m_pEditorFileMonitor.get(); } -void CEditorImpl::RegisterEventLoopHook(IEventLoopHook* pHook) -{ - CCryEditApp::instance()->RegisterEventLoopHook(pHook); -} - -void CEditorImpl::UnregisterEventLoopHook(IEventLoopHook* pHook) -{ - CCryEditApp::instance()->UnregisterEventLoopHook(pHook); -} - float CEditorImpl::GetTerrainElevation(float x, float y) { float terrainElevation = AzFramework::Terrain::TerrainDataRequests::GetDefaultTerrainHeight(); diff --git a/Code/Editor/IEditorImpl.h b/Code/Editor/IEditorImpl.h index 5e47a76802..73fcec917d 100644 --- a/Code/Editor/IEditorImpl.h +++ b/Code/Editor/IEditorImpl.h @@ -155,8 +155,6 @@ public: CMusicManager* GetMusicManager() override { return m_pMusicManager; }; IEditorFileMonitor* GetFileMonitor() override; - void RegisterEventLoopHook(IEventLoopHook* pHook) override; - void UnregisterEventLoopHook(IEventLoopHook* pHook) override; IIconManager* GetIconManager() override; float GetTerrainElevation(float x, float y) override; Editor::EditorQtApplication* GetEditorQtApplication() override { return m_QtApplication; } diff --git a/Code/Editor/Include/IEventLoopHook.h b/Code/Editor/Include/IEventLoopHook.h deleted file mode 100644 index eaf7bf2d62..0000000000 --- a/Code/Editor/Include/IEventLoopHook.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - - -#ifndef CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H -#define CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H -#pragma once - -struct IEventLoopHook -{ - IEventLoopHook* pNextHook; - - IEventLoopHook() - : pNextHook(0) {} - - virtual bool PrePumpMessage() { return false; } -}; - -#endif // CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H diff --git a/Code/Editor/Lib/Tests/IEditorMock.h b/Code/Editor/Lib/Tests/IEditorMock.h index 99c05c7f4d..780362cb30 100644 --- a/Code/Editor/Lib/Tests/IEditorMock.h +++ b/Code/Editor/Lib/Tests/IEditorMock.h @@ -97,8 +97,6 @@ public: MOCK_METHOD0(GetActiveView, class CViewport* ()); MOCK_METHOD1(SetActiveView, void(CViewport*)); MOCK_METHOD0(GetFileMonitor, struct IEditorFileMonitor* ()); - MOCK_METHOD1(RegisterEventLoopHook, void(IEventLoopHook* )); - MOCK_METHOD1(UnregisterEventLoopHook, void(IEventLoopHook* )); MOCK_CONST_METHOD0(CreateQMimeData, QMimeData* ()); MOCK_CONST_METHOD1(DestroyQMimeData, void(QMimeData*)); MOCK_METHOD0(GetLevelIndependentFileMan, class CLevelIndependentFileMan* ()); diff --git a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Android.cpp b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Android.cpp index ea65cf3e59..c51bfaf04e 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Android.cpp +++ b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Android.cpp @@ -209,12 +209,10 @@ namespace ProjectSettingsTool ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::PackageName)) ->Attribute(Attributes::LinkOptional, true) ->Attribute(Attributes::PropertyIdentfier, Identfiers::AndroidPackageName) - ->Attribute(Attributes::LinkedProperty, Identfiers::IosBundleIdentifer) ->DataElement(Handlers::LinkedLineEdit, &AndroidSettings::m_versionName, "Version Name", "Human readable version number. Used to set the \"android: versionName\" tag in the AndroidManifest.xml and ultimately what will be displayed in the App Store.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber)) ->Attribute(Attributes::LinkOptional, true) ->Attribute(Attributes::PropertyIdentfier, Identfiers::AndroidVersionName) - ->Attribute(Attributes::LinkedProperty, Identfiers::IosVersionName) ->DataElement(AZ::Edit::UIHandlers::Default, &AndroidSettings::m_versionNumber, "Version Number", "Internal application version number. Used to set the \"android:versionCode\" tag in the AndroidManifest.xml.") ->Attribute(AZ::Edit::Attributes::Min, 1) ->Attribute(AZ::Edit::Attributes::Max, Validators::maxAndroidVersion) diff --git a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Base.cpp b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Base.cpp index eff524b13e..d011289272 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Base.cpp +++ b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Base.cpp @@ -37,19 +37,15 @@ namespace ProjectSettingsTool ->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_projectName, "Project Name", "The name of the project.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::ProjectName) - ->Attribute(Attributes::LinkedProperty, Identfiers::IosBundleName) ->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_productName, "Product Name", "The project's user facing name.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IsNotEmpty)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::ProductName) - ->Attribute(Attributes::LinkedProperty, Identfiers::IosDisplayName) ->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_executableName, "Executable Name", "The project launcher's name.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::ExecutableName) - ->Attribute(Attributes::LinkedProperty, Identfiers::IosExecutableName) ->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_projectPath, "Project Path", "The project root folder path .") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileNameOrEmpty)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::ProductName) - ->Attribute(Attributes::LinkedProperty, Identfiers::ExecutableName) ->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_projectOutputFolder, "Output Folder", "The folder the packed project will be exported to.") ->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_codeFolder, "Code Folder (legacy)", "A legacy setting specifing the folder for this project's code.") ; diff --git a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Ios.cpp b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Ios.cpp index fd6aef019e..739a5c94da 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Ios.cpp +++ b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_Ios.cpp @@ -262,27 +262,22 @@ namespace ProjectSettingsTool ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleName, "Bundle Name", "The name of the bundle.") - ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName)) + ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSFileName)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::IosBundleName) - ->Attribute(Attributes::LinkedProperty, Identfiers::ProjectName) ->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleDisplayName, "Display Name", "The user visible name of the bundle.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IsNotEmpty)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::IosDisplayName) - ->Attribute(Attributes::LinkedProperty, Identfiers::ProductName) ->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_executableName, "Executable Name", "Name of the bundle's executable file.") - ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName)) + ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSFileName)) ->Attribute(Attributes::PropertyIdentfier, Identfiers::IosExecutableName) - ->Attribute(Attributes::LinkedProperty, Identfiers::ExecutableName) ->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleIdentifier, "Bundle Identifier", "Uniquely identifies the bundle. Should be in reverse-DNS format.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::PackageName)) ->Attribute(Attributes::LinkOptional, true) ->Attribute(Attributes::PropertyIdentfier, Identfiers::IosBundleIdentifer) - ->Attribute(Attributes::LinkedProperty, Identfiers::AndroidPackageName) ->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_versionName, "Version Name", "The release version number string for the app. Displayed in the app store.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber)) ->Attribute(Attributes::LinkOptional, true) ->Attribute(Attributes::PropertyIdentfier, Identfiers::IosVersionName) - ->Attribute(Attributes::LinkedProperty, Identfiers::AndroidVersionName) ->DataElement(Handlers::QValidatedLineEdit, &IosSettings::m_versionNumber, "Version Number", "The build version number string for the bundle.") ->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber)) ->DataElement(AZ::Edit::UIHandlers::ComboBox, &IosSettings::m_developmentRegion, "Development Region", "The default language and region for the app.") diff --git a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_common.h b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_common.h index ab15b2d3cd..ffc9625b53 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_common.h +++ b/Code/Editor/Plugins/ProjectSettingsTool/PlatformSettings_common.h @@ -22,7 +22,6 @@ namespace ProjectSettingsTool static const AZ::Crc32 Obfuscated = AZ_CRC("ObfuscatedText"); // Used as a tooltip and for distinguising linked properties static const AZ::Crc32 PropertyIdentfier = AZ_CRC("PropertyIdentfier"); - static const AZ::Crc32 LinkedProperty = AZ_CRC("LinkedProperty"); static const AZ::Crc32 DefaultPath = AZ_CRC("DefaultPath"); static const AZ::Crc32 DefaultImagePreview = AZ_CRC("DefaultImagePreview"); static const AZ::Crc32 ObfuscatedText = AZ_CRC("ObfuscatedText"); diff --git a/Code/Editor/Plugins/ProjectSettingsTool/PropertyLinked.cpp b/Code/Editor/Plugins/ProjectSettingsTool/PropertyLinked.cpp index b48155f1f8..118c11b739 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/PropertyLinked.cpp +++ b/Code/Editor/Plugins/ProjectSettingsTool/PropertyLinked.cpp @@ -225,25 +225,6 @@ namespace ProjectSettingsTool } } } - else if (attrib == Attributes::LinkedProperty) - { - AZStd::string linked; - if (attrValue->Read(linked)) - { - auto result = m_ctrlToIdentAndLink.find(GUI); - if (result != m_ctrlToIdentAndLink.end()) - { - result->second.linkedIdentifier = linked; - } - else - { - m_ctrlToIdentAndLink.insert(AZStd::pair(GUI, IdentAndLink{ "", linked })); - m_ctrlInitOrder.push_back(GUI); - } - - GUI->SetLinkTooltip(linked.data()); - } - } else { GUI->ConsumeAttribute(attrib, attrValue, debugName); diff --git a/Code/Editor/Plugins/ProjectSettingsTool/Validators.cpp b/Code/Editor/Plugins/ProjectSettingsTool/Validators.cpp index 4dfc4ee25a..5bf3f5ce67 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/Validators.cpp +++ b/Code/Editor/Plugins/ProjectSettingsTool/Validators.cpp @@ -106,6 +106,11 @@ namespace ProjectSettingsTool return RegularExpressionValidator("[\\w,-]+", name); } + // Returns true if valid iOS file or directory name + RetType IOSFileName(const QString& name) + { + return RegularExpressionValidator("[\\w,-.]+", name); + } RetType FileNameOrEmpty(const QString& name) { if (IsNotEmpty(name).first == QValidator::Acceptable) diff --git a/Code/Editor/Plugins/ProjectSettingsTool/Validators.h b/Code/Editor/Plugins/ProjectSettingsTool/Validators.h index bd0abb19d6..7cf3498eef 100644 --- a/Code/Editor/Plugins/ProjectSettingsTool/Validators.h +++ b/Code/Editor/Plugins/ProjectSettingsTool/Validators.h @@ -24,6 +24,8 @@ namespace ProjectSettingsTool // Returns true if valid cross platform file or directory name FunctorValidator::ReturnType FileName(const QString& name); + // Returns true if valid iOS file or directory name + FunctorValidator::ReturnType IOSFileName(const QString& name); // Returns true if valid cross platform file or directory name or empty FunctorValidator::ReturnType FileNameOrEmpty(const QString& name); // Returns true if string isn't empty diff --git a/Code/Editor/editor_lib_files.cmake b/Code/Editor/editor_lib_files.cmake index 75962fe3b3..7908d1e720 100644 --- a/Code/Editor/editor_lib_files.cmake +++ b/Code/Editor/editor_lib_files.cmake @@ -270,7 +270,6 @@ set(FILES Include/ICommandManager.h Include/IDisplayViewport.h Include/IEditorClassFactory.h - Include/IEventLoopHook.h Include/IExportManager.h Include/IGizmoManager.h Include/IIconManager.h diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.cpp index 196ce28216..f7e41c0c00 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.cpp @@ -203,7 +203,7 @@ namespace AZ JsonSerializationResult::Result JsonMapSerializer::LoadElement(void* outputValue, SerializeContext::IDataContainer* container, const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer, const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement, - const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context) + const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap) { namespace JSR = JsonSerializationResult; @@ -231,8 +231,30 @@ namespace AZ return context.Report(keyResult, "Failed to read key for associative container."); } + void* valueAddress = nullptr; + bool keyExists = false; + + // For multimaps, we append values to keys instead updating them. + // This is to ensure legacy multimap serialization support. + if (!isMultiMap) + { + auto associativeContainer = container->GetAssociativeContainerInterface(); + void* existingKeyValuePair = associativeContainer->GetElementByKey(outputValue, keyElement, keyAddress); + if (existingKeyValuePair) + { + valueAddress = pairContainer->GetElementByIndex(existingKeyValuePair, pairElement, 1); + expectedSize--; + keyExists = true; + } + } + + // If the key doesn't exist or it's a multimap, we're adding the new element we reserved above. + if (!keyExists) + { + valueAddress = pairContainer->GetElementByIndex(address, pairElement, 1); + } + // Load value - void* valueAddress = pairContainer->GetElementByIndex(address, pairElement, 1); AZ_Assert(valueAddress, "Element reserved for associative container, but unable to retrieve address of the value."); ContinuationFlags valueLoadFlags = ContinuationFlags::LoadAsNewInstance; if (valueElement->m_flags & SerializeContext::ClassElement::Flags::FLG_POINTER) @@ -257,7 +279,18 @@ namespace AZ } else { - container->StoreElement(outputValue, address); + // Even if the key exists, calling StoreElement will not replace the existing key + // and will free the temporary address as expected. Checking if the key already + // exists and skipping the call to StoreElement if it does, makes the intent more + // clear. The end result is the same either way. + if (!keyExists) + { + container->StoreElement(outputValue, address); + } + else + { + container->FreeReservedElement(outputValue, address, context.GetSerializeContext()); + } if (container->Size(outputValue) != expectedSize) { return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Unavailable, @@ -430,7 +463,7 @@ namespace AZ JsonSerializationResult::Result JsonUnorderedMultiMapSerializer::LoadElement(void* outputValue, SerializeContext::IDataContainer* container, const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer, const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement, - const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context) + const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, [[maybe_unused]] bool isMultiMap) { namespace JSR = JsonSerializationResult; @@ -440,7 +473,7 @@ namespace AZ for (auto& entry : value.GetArray()) { result.Combine(JsonMapSerializer::LoadElement(outputValue, container, pairElement, pairContainer, - keyElement, valueElement, key, entry, context)); + keyElement, valueElement, key, entry, context, true)); if (result.GetProcessing() == JSR::Processing::Halted) { return context.Report(result, "Unable to process the key or all values in multi-map."); @@ -451,7 +484,7 @@ namespace AZ else if (IsExplicitDefault(value)) { return JsonMapSerializer::LoadElement(outputValue, container, pairElement, pairContainer, - keyElement, valueElement, key, value, context); + keyElement, valueElement, key, value, context, true); } else { diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.h b/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.h index 937c8389ff..74d7e81ac9 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/MapSerializer.h @@ -32,7 +32,7 @@ namespace AZ virtual JsonSerializationResult::Result LoadElement(void* outputValue, SerializeContext::IDataContainer* container, const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer, const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement, - const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context); + const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap = false); virtual JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context, bool sortResult); @@ -62,7 +62,7 @@ namespace AZ JsonSerializationResult::Result LoadElement(void* outputValue, SerializeContext::IDataContainer* container, const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer, const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement, - const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context) override; + const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap = false) override; using JsonMapSerializer::Store; JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue, diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/MapSerializerTests.cpp b/Code/Framework/AzCore/Tests/Serialization/Json/MapSerializerTests.cpp index 7ffe3ffee0..fe97d36f08 100644 --- a/Code/Framework/AzCore/Tests/Serialization/Json/MapSerializerTests.cpp +++ b/Code/Framework/AzCore/Tests/Serialization/Json/MapSerializerTests.cpp @@ -475,7 +475,7 @@ namespace JsonSerializationTests EXPECT_STRCASEEQ("value_42", worldKey->second.m_value.c_str()); } - TEST_F(JsonMapSerializerTests, Load_DuplicateKey_EntryIgnored) + TEST_F(JsonMapSerializerTests, Load_DuplicateKey_EntryUpdated) { using namespace AZ::JsonSerializationResult; @@ -489,12 +489,12 @@ namespace JsonSerializationTests StringMap values; ResultCode result = m_unorderedMapSerializer.Load(&values, azrtti_typeid(&values), *m_jsonDocument, *m_jsonDeserializationContext); - EXPECT_EQ(Processing::PartialAlter, result.GetProcessing()); - EXPECT_EQ(Outcomes::Unavailable, result.GetOutcome()); + EXPECT_EQ(Processing::Completed, result.GetProcessing()); + EXPECT_EQ(Outcomes::Success, result.GetOutcome()); auto entry = values.find("Hello"); ASSERT_NE(values.end(), entry); - EXPECT_STRCASEEQ("World", entry->second.c_str()); + EXPECT_EQ("Other", entry->second); } TEST_F(JsonMapSerializerTests, Load_DuplicateMultiKey_LoadEverything) @@ -536,8 +536,8 @@ namespace JsonSerializationTests ResultCode result = m_unorderedMapSerializer.Load(&values, azrtti_typeid(&values), *m_jsonDocument, *m_jsonDeserializationContext); - EXPECT_EQ(Processing::Altered, result.GetProcessing()); - EXPECT_EQ(Outcomes::Unavailable, result.GetOutcome()); + EXPECT_EQ(Processing::Completed, result.GetProcessing()); + EXPECT_EQ(Outcomes::Success, result.GetOutcome()); auto entry = values.find("Hello"); ASSERT_NE(values.end(), entry); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 93e4ffd3da..45363f4bb5 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -80,7 +80,6 @@ namespace AzToolsFramework EntityOutlinerListModel::EntityOutlinerListModel(QObject* parent) : QAbstractItemModel(parent) , m_entitySelectQueue() - , m_entityExpandQueue() , m_entityChangeQueue() , m_entityChangeQueued(false) , m_entityLayoutQueued(false) @@ -1275,7 +1274,6 @@ namespace AzToolsFramework void EntityOutlinerListModel::QueueEntityToExpand(AZ::EntityId entityId, bool expand) { m_entityExpansionState[entityId] = expand; - m_entityExpandQueue.insert(entityId); QueueEntityUpdate(entityId); } @@ -1300,16 +1298,7 @@ namespace AzToolsFramework { return; } - - { - AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:ExpandQueue"); - for (auto entityId : m_entityExpandQueue) - { - emit ExpandEntity(entityId, IsExpanded(entityId)); - }; - m_entityExpandQueue.clear(); - } - + { AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:SelectQueue"); for (auto entityId : m_entitySelectQueue) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx index f099ed504a..51d047e81a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx @@ -156,7 +156,6 @@ namespace AzToolsFramework void ProcessEntityUpdates(); Q_SIGNALS: - void ExpandEntity(const AZ::EntityId& entityId, bool expand); void SelectEntity(const AZ::EntityId& entityId, bool select); void EnableSelectionUpdates(bool enable); void ResetFilter(); @@ -190,7 +189,6 @@ namespace AzToolsFramework void QueueEntityToExpand(AZ::EntityId entityId, bool expand); void ProcessEntityInfoResetEnd(); AZStd::unordered_set m_entitySelectQueue; - AZStd::unordered_set m_entityExpandQueue; AZStd::unordered_set m_entityChangeQueue; bool m_entityChangeQueued; bool m_entityLayoutQueued; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp index 857fdb5f80..c2bbebb72e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp @@ -81,6 +81,61 @@ namespace AzToolsFramework update(); } + void EntityOutlinerTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) + { + AzQtComponents::StyledTreeView::dataChanged(topLeft, bottomRight, roles); + + if (topLeft.isValid() && topLeft.parent() == bottomRight.parent() && topLeft.row() <= bottomRight.row() && + topLeft.column() <= bottomRight.column()) + { + for (int i = topLeft.row(); i <= bottomRight.row(); i++) + { + auto modelRow = topLeft.sibling(i, EntityOutlinerListModel::ColumnName); + if (modelRow.isValid()) + { + checkExpandedState(modelRow); + } + } + } + } + + void EntityOutlinerTreeView::rowsInserted(const QModelIndex& parent, int start, int end) + { + if (parent.isValid()) + { + for (int i = start; i <= end; i++) + { + auto modelRow = model()->index(i, EntityOutlinerListModel::ColumnName, parent); + if (modelRow.isValid()) + { + checkExpandedState(modelRow); + recursiveCheckExpandedStates(modelRow); + } + } + } + AzQtComponents::StyledTreeView::rowsInserted(parent, start, end); + } + + void EntityOutlinerTreeView::recursiveCheckExpandedStates(const QModelIndex& current) + { + const int rowCount = model()->rowCount(current); + for (int i = 0; i < rowCount; i++) + { + auto modelRow = model()->index(i, EntityOutlinerListModel::ColumnName, current); + if (modelRow.isValid()) + { + checkExpandedState(modelRow); + recursiveCheckExpandedStates(modelRow); + } + } + } + + void EntityOutlinerTreeView::checkExpandedState(const QModelIndex& current) + { + const bool expandState = current.data(EntityOutlinerListModel::ExpandedRole).template value(); + setExpanded(current, expandState); + } + void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event) { //postponing normal mouse pressed logic until mouse is released or dragged diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx index 9262da9a73..014bb7bd48 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx @@ -51,6 +51,10 @@ namespace AzToolsFramework Q_SIGNALS: void ItemDropped(); + protected Q_SLOTS: + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; + void rowsInserted(const QModelIndex &parent, int start, int end) override; + protected: // Qt overrides void mousePressEvent(QMouseEvent* event) override; @@ -75,6 +79,8 @@ namespace AzToolsFramework void ClearQueuedMouseEvent(); void processQueuedMousePressedEvent(QMouseEvent* event); + void recursiveCheckExpandedStates(const QModelIndex& parent); + void checkExpandedState(const QModelIndex& current); void StartCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp index 31bb067603..52b688543a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp @@ -224,7 +224,6 @@ namespace AzToolsFramework connect(m_gui->m_objectTree, &QTreeView::expanded, this, &EntityOutlinerWidget::OnTreeItemExpanded); connect(m_gui->m_objectTree, &QTreeView::collapsed, this, &EntityOutlinerWidget::OnTreeItemCollapsed); connect(m_gui->m_objectTree, &EntityOutlinerTreeView::ItemDropped, this, &EntityOutlinerWidget::OnDropEvent); - connect(m_listModel, &EntityOutlinerListModel::ExpandEntity, this, &EntityOutlinerWidget::OnExpandEntity); connect(m_listModel, &EntityOutlinerListModel::SelectEntity, this, &EntityOutlinerWidget::OnSelectEntity); connect(m_listModel, &EntityOutlinerListModel::EnableSelectionUpdates, this, &EntityOutlinerWidget::OnEnableSelectionUpdates); connect(m_listModel, &EntityOutlinerListModel::ResetFilter, this, &EntityOutlinerWidget::ClearFilter); @@ -972,10 +971,6 @@ namespace AzToolsFramework m_listModel->OnEntityCollapsed(entityId); } - void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand) - { - m_gui->m_objectTree->setExpanded(GetIndexFromEntityId(entityId), expand); - } void EntityOutlinerWidget::OnSelectEntity(const AZ::EntityId& entityId, bool selected) { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.hxx index e6c42fa64e..38d2e16199 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.hxx @@ -155,7 +155,6 @@ namespace AzToolsFramework void OnTreeItemDoubleClicked(const QModelIndex& index); void OnTreeItemExpanded(const QModelIndex& index); void OnTreeItemCollapsed(const QModelIndex& index); - void OnExpandEntity(const AZ::EntityId& entityId, bool expand); void OnSelectEntity(const AZ::EntityId& entityId, bool selected); void OnEnableSelectionUpdates(bool enable); void OnDropEvent(); diff --git a/Code/Legacy/CryCommon/WinBase.cpp b/Code/Legacy/CryCommon/WinBase.cpp index 771cde324e..6e6f5e210a 100644 --- a/Code/Legacy/CryCommon/WinBase.cpp +++ b/Code/Legacy/CryCommon/WinBase.cpp @@ -856,18 +856,4 @@ DLL_EXPORT void OutputDebugString(const char* outputString) #endif -// This code does not have a long life span and will be replaced soon -#if defined(APPLE) || defined(LINUX) || defined(DEFINE_LEGACY_CRY_FILE_OPERATIONS) - -bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes) -{ - //TODO: implement - printf("CrySetFileAttributes not properly implemented yet\n"); - return false; -} - - - -#endif //defined(APPLE) || defined(LINUX) - #endif // AZ_TRAIT_LEGACY_CRYCOMMON_USE_WINDOWS_STUBS diff --git a/Code/Legacy/CryCommon/platform.h b/Code/Legacy/CryCommon/platform.h index d2251c7091..512f8b4892 100644 --- a/Code/Legacy/CryCommon/platform.h +++ b/Code/Legacy/CryCommon/platform.h @@ -336,7 +336,6 @@ void SetFlags(T& dest, U flags, bool b) #include AZ_RESTRICTED_FILE(platform_h) #endif -bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes); threadID CryGetCurrentThreadId(); #ifdef __GNUC__ diff --git a/Code/Legacy/CryCommon/platform_impl.cpp b/Code/Legacy/CryCommon/platform_impl.cpp index 8cbc58ad95..3392c40771 100644 --- a/Code/Legacy/CryCommon/platform_impl.cpp +++ b/Code/Legacy/CryCommon/platform_impl.cpp @@ -24,7 +24,6 @@ #define PLATFORM_IMPL_H_SECTION_TRAITS 1 #define PLATFORM_IMPL_H_SECTION_CRYLOWLATENCYSLEEP 2 #define PLATFORM_IMPL_H_SECTION_CRYGETFILEATTRIBUTES 3 -#define PLATFORM_IMPL_H_SECTION_CRYSETFILEATTRIBUTES 4 #define PLATFORM_IMPL_H_SECTION_CRY_FILE_ATTRIBUTE_STUBS 5 #define PLATFORM_IMPL_H_SECTION_CRY_SYSTEM_FUNCTIONS 6 #define PLATFORM_IMPL_H_SECTION_VIRTUAL_ALLOCATORS 7 @@ -238,22 +237,6 @@ void InitRootDir(char szExeFileName[], uint nExeSize, char szExeRootName[], uint } } -////////////////////////////////////////////////////////////////////////// -bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes) -{ -#if defined(AZ_RESTRICTED_PLATFORM) - #define AZ_RESTRICTED_SECTION PLATFORM_IMPL_H_SECTION_CRYSETFILEATTRIBUTES - #include AZ_RESTRICTED_FILE(platform_impl_h) -#endif -#if defined(AZ_RESTRICTED_SECTION_IMPLEMENTED) -#undef AZ_RESTRICTED_SECTION_IMPLEMENTED -#else - AZStd::wstring lpFileNameW; - AZStd::to_wstring(lpFileNameW, lpFileName); - return SetFileAttributes(lpFileNameW.c_str(), dwFileAttributes) != 0; -#endif -} - ////////////////////////////////////////////////////////////////////////// threadID CryGetCurrentThreadId() { diff --git a/Code/Legacy/CrySystem/XML/xml.cpp b/Code/Legacy/CrySystem/XML/xml.cpp index fb0714500c..2356e71518 100644 --- a/Code/Legacy/CrySystem/XML/xml.cpp +++ b/Code/Legacy/CrySystem/XML/xml.cpp @@ -1132,7 +1132,10 @@ bool CXmlNode::saveToFile(const char* fileName) bool CXmlNode::saveToFile([[maybe_unused]] const char* fileName, size_t chunkSize, AZ::IO::HandleType fileHandle) { - CrySetFileAttributes(fileName, FILE_ATTRIBUTE_NORMAL); + if (AZ::IO::SystemFile::Exists(fileName) && !AZ::IO::SystemFile::IsWritable(fileName)) + { + AZ::IO::SystemFile::SetWritable(fileName, true); + } if (chunkSize < 256 * 1024) // make at least 256k { diff --git a/Code/Tools/AssetProcessor/assetprocessor_test_files.cmake b/Code/Tools/AssetProcessor/assetprocessor_test_files.cmake index 20ab4b706d..bc7b16cc79 100644 --- a/Code/Tools/AssetProcessor/assetprocessor_test_files.cmake +++ b/Code/Tools/AssetProcessor/assetprocessor_test_files.cmake @@ -30,6 +30,8 @@ set(FILES native/tests/assetBuilderSDK/SerializationDependenciesTests.cpp native/tests/assetmanager/AssetProcessorManagerTest.cpp native/tests/assetmanager/AssetProcessorManagerTest.h + native/tests/assetmanager/ModtimeScanningTests.cpp + native/tests/assetmanager/ModtimeScanningTests.h native/tests/utilities/assetUtilsTest.cpp native/tests/platformconfiguration/platformconfigurationtests.cpp native/tests/platformconfiguration/platformconfigurationtests.h diff --git a/Code/Tools/AssetProcessor/native/tests/PathDependencyManagerTests.cpp b/Code/Tools/AssetProcessor/native/tests/PathDependencyManagerTests.cpp index 1d6e92e47e..5103643833 100644 --- a/Code/Tools/AssetProcessor/native/tests/PathDependencyManagerTests.cpp +++ b/Code/Tools/AssetProcessor/native/tests/PathDependencyManagerTests.cpp @@ -49,7 +49,7 @@ namespace UnitTests } struct PathDependencyBase - : UnitTest::TraceBusRedirector + : ::UnitTest::TraceBusRedirector { void Init(); void Destroy(); @@ -65,7 +65,7 @@ namespace UnitTests }; struct PathDependencyDeletionTest - : UnitTest::ScopedAllocatorSetupFixture + : ::UnitTest::ScopedAllocatorSetupFixture , PathDependencyBase { void SetUp() override @@ -357,7 +357,7 @@ namespace UnitTests } struct PathDependencyBenchmarks - : UnitTest::ScopedAllocatorFixture + : ::UnitTest::ScopedAllocatorFixture , PathDependencyBase { static inline constexpr int NumTestDependencies = 4; // Must be a multiple of 4 @@ -530,7 +530,7 @@ namespace UnitTests BENCHMARK_F(PathDependencyBenchmarksWrapperClass, BM_DeferredWildcardDependencyResolution)(benchmark::State& state) { - for (auto _ : state) + for ([[maybe_unused]] auto unused : state) { m_benchmarks->m_stateData->SetProductDependencies(m_benchmarks->m_dependencies); diff --git a/Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp b/Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp index 8d52ba23bd..bd2ef8b3cd 100644 --- a/Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp +++ b/Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp @@ -191,7 +191,7 @@ namespace UnitTests m_data->m_perforceComponent = AZStd::make_unique(); m_data->m_perforceComponent->Activate(); - m_data->m_perforceComponent->SetConnection(new UnitTest::MockPerforceConnection(m_command)); + m_data->m_perforceComponent->SetConnection(new ::UnitTest::MockPerforceConnection(m_command)); } void TearDown() override @@ -876,7 +876,7 @@ namespace UnitTests QDir tempPath(m_tempDir.path()); auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("duplicate/file1.tif"); - + ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData())); auto result = m_data->m_reporter->Delete(filePath.toUtf8().constData(), false); diff --git a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp index 0908d0fdb9..347d2a6842 100644 --- a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp +++ b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp @@ -22,108 +22,6 @@ using namespace AssetProcessor; -class AssetProcessorManager_Test - : public AssetProcessorManager -{ -public: - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, AssetProcessedImpl_DifferentProductDependenciesPerProduct_SavesCorrectlyToDatabase); - - friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies); - friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution); - friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, SameFilenameForAllPlatforms); - - friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath); - - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles); - - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTest); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDifferentTypes_BasicTest); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Reverse_BasicTest); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_MissingFiles_ReturnsNoPathWithPlaceholders); - - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_BeforeComputingDirtiness_AllDirty); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_EmptyDatabase_AllDirty); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_SameAsLastTime_NoneDirty); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_MoreThanLastTime_NewOneIsDirty); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_FewerThanLastTime_Dirty); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPattern_CountsAsNew); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPatternType_CountsAsNew); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewPattern_CountsAsNewBuilder); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewVersionNumber_IsNotANewBuilder); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewAnalysisFingerprint_IsNotANewBuilder); - - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_BasicTest); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_UpdateTest); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid_UpdatesWhenTheyAppear); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName_UpdatesWhenTheyAppear); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardMissingFiles_ByName_UpdatesWhenTheyAppear); - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, JobDependencyOrderOnce_MultipleJobs_EmitOK); - - friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint); - - friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_DoesNotDuplicateDependency); - friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, AbsolutePathProductDependency_RetryDeferredDependenciesWithMatchingSource_DependencyResolves); - friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap); - friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap); - - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping); - - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged); - - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform); - - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile); - friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_DeleteFile); - friend class GTEST_TEST_CLASS_NAME_(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache); - friend class GTEST_TEST_CLASS_NAME_(MetadataFileTest, MetadataFile_SourceFileExtensionDifferentCase); - - friend class AssetProcessorManagerTest; - friend struct ModtimeScanningTest; - friend struct JobDependencyTest; - friend struct ChainJobDependencyTest; - friend struct DeleteTest; - friend struct PathDependencyTest; - friend struct DuplicateProductsTest; - friend struct DuplicateProcessTest; - friend struct AbsolutePathProductDependencyTest; - friend struct WildcardSourceDependencyTest; - - explicit AssetProcessorManager_Test(PlatformConfiguration* config, QObject* parent = nullptr); - ~AssetProcessorManager_Test() override; - - bool CheckJobKeyToJobRunKeyMap(AZStd::string jobKey); - - int CountDirtyBuilders() const - { - int numDirty = 0; - for (const auto& element : m_builderDataCache) - { - if (element.second.m_isDirty) - { - ++numDirty; - } - } - return numDirty; - } - - bool IsBuilderDirty(const AZ::Uuid& builderBusId) const - { - auto finder = m_builderDataCache.find(builderBusId); - if (finder == m_builderDataCache.end()) - { - return true; - } - return finder->second.m_isDirty; - } -}; - AssetProcessorManager_Test::AssetProcessorManager_Test(AssetProcessor::PlatformConfiguration* config, QObject* parent /*= 0*/) :AssetProcessorManager(config, parent) { @@ -3839,632 +3737,6 @@ TEST_F(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint) ASSERT_EQ(source.m_analysisFingerprint, ""); } -void ModtimeScanningTest::SetUp() -{ - AssetProcessorManagerTest::SetUp(); - - m_data = AZStd::make_unique(); - - // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own - m_mockApplicationManager->BusDisconnect(); - - m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc("test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) }); - m_data->m_mockBuilderInfoHandler.BusConnect(); - - ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder)); - - // Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping - m_assetProcessorManager->ComputeBuilderDirty(); - m_assetProcessorManager->ComputeBuilderDirty(); - - auto assetConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, [this](JobDetails details) - { - m_data->m_processResults.push_back(AZStd::move(details)); - }); - - auto deletedConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted, [this](QString file) - { - m_data->m_deletedSources.push_back(file); - }); - - // Create the test file - const auto& scanFolder = m_config->GetScanFolderAt(0); - m_data->m_relativePathFromWatchFolder[0] = "modtimeTestFile.txt"; - m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[0])); - - m_data->m_relativePathFromWatchFolder[1] = "modtimeTestDependency.txt"; - m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[1])); - - m_data->m_relativePathFromWatchFolder[2] = "modtimeTestDependency.txt.assetinfo"; - m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[2])); - - for (const auto& path : m_data->m_absolutePath) - { - ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path, "")); - } - - m_data->m_mockBuilderInfoHandler.m_dependencyFilePath = m_data->m_absolutePath[1].toUtf8().data(); - - // Add file to database with no modtime - { - AssetDatabaseConnection connection; - ASSERT_TRUE(connection.OpenDatabase()); - AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; - fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[0].toUtf8().data(); - fileEntry.m_modTime = 0; - fileEntry.m_isFolder = false; - fileEntry.m_scanFolderPK = scanFolder.ScanFolderID(); - - bool entryAlreadyExists; - ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); - ASSERT_FALSE(entryAlreadyExists); - - fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry - fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[1].toUtf8().data(); - ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); - ASSERT_FALSE(entryAlreadyExists); - - fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry - fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[2].toUtf8().data(); - ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); - ASSERT_FALSE(entryAlreadyExists); - } - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ASSERT_TRUE(BlockUntilIdle(5000)); - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); - ASSERT_EQ(m_data->m_processResults.size(), 2); - ASSERT_EQ(m_data->m_deletedSources.size(), 0); - - ProcessAssetJobs(); - - m_data->m_processResults.clear(); - m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; - - m_isIdling = false; -} - -void ModtimeScanningTest::TearDown() -{ - m_data = nullptr; - - AssetProcessorManagerTest::TearDown(); -} - -void ModtimeScanningTest::ProcessAssetJobs() -{ - m_data->m_productPaths.clear(); - - for (const auto& processResult : m_data->m_processResults) - { - auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1"); - m_data->m_productPaths.emplace( - QDir(processResult.m_jobEntry.m_watchFolderPath) - .absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName) - .toUtf8() - .constData(), - file); - - // Create the file on disk - ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products.")); - - AssetBuilderSDK::ProcessJobResponse response; - response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success; - response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1)); - - QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry), Q_ARG(AssetBuilderSDK::ProcessJobResponse, response)); - } - - ASSERT_TRUE(BlockUntilIdle(5000)); - - m_isIdling = false; -} - -void ModtimeScanningTest::SimulateAssetScanner(QSet filePaths) -{ - QMetaObject::invokeMethod(m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started)); - QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet, filePaths)); - QMetaObject::invokeMethod(m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed)); -} - -QSet ModtimeScanningTest::BuildFileSet() -{ - QSet filePaths; - - for (const auto& path : m_data->m_absolutePath) - { - QFileInfo fileInfo(path); - auto modtime = fileInfo.lastModified(); - AZ::u64 fileSize = fileInfo.size(); - filePaths.insert(AssetFileInfo(path, modtime, fileSize, m_config->GetScanFolderForFile(path), false)); - } - - return filePaths; -} - -void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs) -{ - ASSERT_TRUE(BlockUntilIdle(5000)); - - EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs); - EXPECT_EQ(m_data->m_processResults.size(), processJobs); - EXPECT_FALSE(m_data->m_processResults[0].m_autoFail); - EXPECT_FALSE(m_data->m_processResults[1].m_autoFail); - EXPECT_EQ(m_data->m_deletedSources.size(), 0); - - m_isIdling = false; -} - -void ModtimeScanningTest::ExpectNoWork() -{ - // Since there's no work to do, the idle event isn't going to trigger, just process events a couple times - for (int i = 0; i < 10; ++i) - { - QCoreApplication::processEvents(QEventLoop::AllEvents, 10); - } - - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0); - ASSERT_EQ(m_data->m_processResults.size(), 0); - ASSERT_EQ(m_data->m_deletedSources.size(), 0); - - m_isIdling = false; -} - -void ModtimeScanningTest::SetFileContents(QString filePath, QString contents) -{ - QFile file(filePath); - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.write(contents.toUtf8().constData()); - file.close(); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping) -{ - using namespace AzToolsFramework::AssetSystem; - - // Make sure modtime skipping is disabled - // We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off - m_assetProcessorManager->m_allowModtimeSkippingFeature = false; - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // 2 create jobs but 0 process jobs because the file has already been processed before in SetUp - ExpectWork(2, 0); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged) -{ - using namespace AzToolsFramework::AssetSystem; - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectNoWork(); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform) -{ - using namespace AzToolsFramework::AssetSystem; - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - // Enable android platform after the initial SetUp has already processed the files for pc - QDir tempPath(m_tempDir.path()); - AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" }); - m_config->EnablePlatform(androidPlatform, true); - - // There's no way to remove scanfolders and adding a new one after enabling the platform will cause the pc assets to build as well, which we don't want - // Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder - auto& platforms = const_cast&>(m_config->GetScanFolderAt(0).GetPlatforms()); - platforms.push_back(androidPlatform); - - // We need the builder fingerprints to be updated to reflect the newly enabled platform - m_assetProcessorManager->ComputeBuilderDirty(); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectWork(4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed) - - ASSERT_TRUE(m_data->m_processResults[0].m_destinationPath.contains("android")); - ASSERT_TRUE(m_data->m_processResults[1].m_destinationPath.contains("android")); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp) -{ - // Update the timestamp on a file without changing its contents - // This should not cause any job to run since the hash of the file is the same before/after - // Additionally, the timestamp stored in the database should be updated - using namespace AzToolsFramework::AssetSystem; - - uint64_t timestamp = 1594923423; - - QString databaseName, scanfolderName; - m_config->ConvertToRelativePath(m_data->m_absolutePath[1], databaseName, scanfolderName); - auto* scanFolder = m_config->GetScanFolderForFile(m_data->m_absolutePath[1]); - - AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; - - m_assetProcessorManager.get()->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry); - - ASSERT_NE(fileEntry.m_modTime, timestamp); - uint64_t existingTimestamp = fileEntry.m_modTime; - - // Modify the timestamp on just one file - AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectNoWork(); - - m_assetProcessorManager.get()->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry); - - // The timestamp should be updated even though nothing processed - ASSERT_NE(fileEntry.m_modTime, existingTimestamp); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile) -{ - // Update the timestamp on a file without changing its contents - // This should not cause any job to run since the hash of the file is the same before/after - // Additionally, the timestamp stored in the database should be updated - using namespace AzToolsFramework::AssetSystem; - - uint64_t timestamp = 1594923423; - - // Modify the timestamp on just one file - AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, false); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectWork(2, 2); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile) -{ - using namespace AzToolsFramework::AssetSystem; - - SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world"); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well - ExpectWork(2, 2); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain) -{ - using namespace AzToolsFramework::AssetSystem; - auto theFile = m_data->m_absolutePath[1].toUtf8(); - const char* theFileString = theFile.constData(); - - SetFileContents(theFileString, "hello world"); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well - ExpectWork(2, 2); - ProcessAssetJobs(); - - m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; - m_data->m_processResults.clear(); - m_data->m_deletedSources.clear(); - - SetFileContents(theFileString, ""); - - filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // Expect processing to happen again - ExpectWork(2, 2); -} - -struct LockedFileTest - : ModtimeScanningTest - , AssetProcessor::ConnectionBus::Handler -{ - MOCK_METHOD3(SendRaw, size_t (unsigned, unsigned, const QByteArray&)); - MOCK_METHOD3(SendPerPlatform, size_t (unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const QString&)); - MOCK_METHOD4(SendRawPerPlatform, size_t (unsigned, unsigned, const QByteArray&, const QString&)); - MOCK_METHOD2(SendRequest, unsigned (const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const ResponseCallback&)); - MOCK_METHOD2(SendResponse, size_t (unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&)); - MOCK_METHOD1(RemoveResponseHandler, void (unsigned)); - - size_t Send(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override - { - using SourceFileNotificationMessage = AzToolsFramework::AssetSystem::SourceFileNotificationMessage; - switch (message.GetMessageType()) - { - case SourceFileNotificationMessage::MessageType: - if (const auto sourceFileMessage = azrtti_cast(&message); sourceFileMessage != nullptr && - sourceFileMessage->m_type == SourceFileNotificationMessage::NotificationType::FileRemoved) - { - // The File Remove message will occur before an attempt to delete the file - // Wait for more than 1 File Remove message. - // This indicates the AP has attempted to delete the file once, failed to do so and is now retrying - ++m_deleteCounter; - - if(m_deleteCounter > 1 && m_callback) - { - m_callback(); - m_callback = {}; // Unset it to be safe, we only intend to run the callback once - } - } - break; - default: - break; - } - - return 0; - } - - void SetUp() override - { - ModtimeScanningTest::SetUp(); - - ConnectionBus::Handler::BusConnect(0); - } - - void TearDown() override - { - ConnectionBus::Handler::BusDisconnect(); - - ModtimeScanningTest::TearDown(); - } - - AZStd::atomic_int m_deleteCounter{ 0 }; - AZStd::function m_callback; -}; - -TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails) -{ - auto theFile = m_data->m_absolutePath[1].toUtf8(); - const char* theFileString = theFile.constData(); - auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString); - - { - QFile file(theFileString); - file.remove(); - } - - ASSERT_GT(m_data->m_productPaths.size(), 0); - QFile product(productPath); - - ASSERT_TRUE(product.open(QIODevice::ReadOnly)); - - // Check if we can delete the file now, if we can't, proceed with the test - // If we can, it means the OS running this test doesn't lock open files so there's nothing to test - if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData())) - { - QMetaObject::invokeMethod( - m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString))); - - EXPECT_TRUE(BlockUntilIdle(5000)); - - EXPECT_TRUE(QFile::exists(productPath)); - EXPECT_EQ(m_data->m_deletedSources.size(), 0); - } - else - { - SUCCEED() << "Skipping test. OS does not lock open files."; - } -} - -TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased) -{ - // This test is intended to verify the AP will successfully retry deleting a source asset - // when one of its product assets is locked temporarily - // We'll lock the file by holding it open - - auto theFile = m_data->m_absolutePath[1].toUtf8(); - const char* theFileString = theFile.constData(); - auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString); - - { - QFile file(theFileString); - file.remove(); - } - - ASSERT_GT(m_data->m_productPaths.size(), 0); - QFile product(productPath); - - // Open the file and keep it open to lock it - // We'll start a thread later to unlock the file - // This will allow us to test how AP handles trying to delete a locked file - ASSERT_TRUE(product.open(QIODevice::ReadOnly)); - - // Check if we can delete the file now, if we can't, proceed with the test - // If we can, it means the OS running this test doesn't lock open files so there's nothing to test - if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData())) - { - m_deleteCounter = 0; - - // Set up a callback which will fire after at least 1 retry - // Unlock the file at that point so AP can successfully delete it - m_callback = [&product]() - { - product.close(); - }; - - QMetaObject::invokeMethod( - m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString))); - - EXPECT_TRUE(BlockUntilIdle(5000)); - - EXPECT_FALSE(QFile::exists(productPath)); - EXPECT_EQ(m_data->m_deletedSources.size(), 1); - - EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file - m_errorAbsorber->ExpectAsserts(0); - } - else - { - SUCCEED() << "Skipping test. OS does not lock open files."; - } -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess) -{ - using namespace AzToolsFramework::AssetSystem; - - SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world"); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well - ExpectWork(2, 2); - ProcessAssetJobs(); - - m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; - m_data->m_processResults.clear(); - m_data->m_deletedSources.clear(); - - // Make file 0 have the same contents as file 1 - SetFileContents(m_data->m_absolutePath[0].toUtf8().constData(), "hello world"); - - filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectWork(1, 1); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile) -{ - using namespace AzToolsFramework::AssetSystem; - - SetFileContents(m_data->m_absolutePath[2].toUtf8().constData(), "hello world"); - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file - // that triggers the source file which is a dependency that triggers the other test file to process as well - ExpectWork(2, 2); -} - -TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile) -{ - using namespace AzToolsFramework::AssetSystem; - - // Enable the features we're testing - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - AssetUtilities::SetUseFileHashOverride(true, true); - - ASSERT_TRUE(QFile::remove(m_data->m_absolutePath[0])); - - // Feed in ONLY one file (the one we didn't delete) - QSet filePaths; - QFileInfo fileInfo(m_data->m_absolutePath[1]); - auto modtime = fileInfo.lastModified(); - AZ::u64 fileSize = fileInfo.size(); - filePaths.insert(AssetFileInfo(m_data->m_absolutePath[1], modtime, fileSize, &m_config->GetScanFolderAt(0), false)); - - SimulateAssetScanner(filePaths); - - QElapsedTimer timer; - timer.start(); - - do - { - QCoreApplication::processEvents(QEventLoop::AllEvents, 10); - } while (m_data->m_deletedSources.size() < m_data->m_relativePathFromWatchFolder[0].size() && timer.elapsed() < 5000); - - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0); - ASSERT_EQ(m_data->m_processResults.size(), 0); - ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_relativePathFromWatchFolder[0])); -} - -TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed) -{ - using namespace AzToolsFramework::AssetSystem; - - m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]); - - ASSERT_TRUE(BlockUntilIdle(5000)); - - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1); - ASSERT_EQ(m_data->m_processResults.size(), 1); -} - -TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess) -{ - using namespace AzToolsFramework::AssetSystem; - - using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry; - - SourceFileDependencyEntry newEntry1; - newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId; - newEntry1.m_builderGuid = AZ::Uuid::CreateRandom(); - newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData(); - newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData(); - newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource; - - m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]); - ASSERT_TRUE(BlockUntilIdle(5000)); - - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1); - ASSERT_EQ(m_data->m_processResults.size(), 1); - - m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[1]); - ASSERT_TRUE(BlockUntilIdle(5000)); - - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3); - ASSERT_EQ(m_data->m_processResults.size(), 3); -} - -TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess) -{ - using namespace AzToolsFramework::AssetSystem; - - const auto& scanFolder = m_config->GetScanFolderAt(0); - - QString scanPath = scanFolder.ScanPath(); - m_assetProcessorManager->RequestReprocess(scanPath); - ASSERT_TRUE(BlockUntilIdle(5000)); - - // two text files are source assets, assetinfo is not - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); - ASSERT_EQ(m_data->m_processResults.size(), 2); -} - ////////////////////////////////////////////////////////////////////////// MockBuilderInfoHandler::~MockBuilderInfoHandler() @@ -5205,130 +4477,7 @@ TEST_F(ChainJobDependencyTest, TestChainDependency_Multi) } } -void DeleteTest::SetUp() -{ - AssetProcessorManagerTest::SetUp(); - m_data = AZStd::make_unique(); - - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - - // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own - m_mockApplicationManager->BusDisconnect(); - - m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc("test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) }); - m_data->m_mockBuilderInfoHandler.BusConnect(); - - ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder)); - - // Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping - m_assetProcessorManager->ComputeBuilderDirty(); - m_assetProcessorManager->ComputeBuilderDirty(); - - auto setupConnectionsFunc = [this]() - { - QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, [this](JobDetails details) - { - m_data->m_processResults.push_back(AZStd::move(details)); - }); - - QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted, [this](QString file) - { - m_data->m_deletedSources.push_back(file); - }); - }; - - auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file) - { - using namespace AzToolsFramework::AssetDatabase; - - QString watchFolderPath = scanFolder->ScanPath(); - QString absPath(QDir(watchFolderPath).absoluteFilePath(file)); - UnitTestUtils::CreateDummyFile(absPath); - - m_data->m_absolutePath.push_back(absPath); - - AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; - fileEntry.m_fileName = file.toUtf8().constData(); - fileEntry.m_modTime = 0; - fileEntry.m_isFolder = false; - fileEntry.m_scanFolderPK = scanFolder->ScanFolderID(); - - bool entryAlreadyExists; - ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists)); - ASSERT_FALSE(entryAlreadyExists); - }; - - setupConnectionsFunc(); - - // Create test files - QDir tempPath(m_tempDir.path()); - const auto* scanFolder1 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder1")); - const auto* scanFolder4 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder4")); - - createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt")); - createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt")); - - // Run the test files through AP all the way to processing stage - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ASSERT_TRUE(BlockUntilIdle(5000)); - ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); - ASSERT_EQ(m_data->m_processResults.size(), 2); - ASSERT_EQ(m_data->m_deletedSources.size(), 0); - - ProcessAssetJobs(); - - m_data->m_processResults.clear(); - m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; - - // Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM - m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get())); - - m_idleConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState, [this](bool newState) - { - m_isIdling = newState; - }); - - setupConnectionsFunc(); - - m_assetProcessorManager->ComputeBuilderDirty(); -} - -TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache) -{ - // There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed - // As a result, deleting a folder didn't count as a "folder", so the wrong code path was taken. This test makes sure the correct deletion events fire - - using namespace AzToolsFramework::AssetSystem; - - // Modtime skipping has to be on for this - m_assetProcessorManager->m_allowModtimeSkippingFeature = true; - - // Feed in the files from the asset scanner, no jobs should run since they're already up-to-date - QSet filePaths = BuildFileSet(); - SimulateAssetScanner(filePaths); - - ExpectNoWork(); - - // Delete one of the folders - QDir tempPath(m_tempDir.path()); - QString absPath(tempPath.absoluteFilePath("subfolder1/textures")); - QDir(absPath).removeRecursively(); - - AZStd::vector deletedFolders; - QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceFolderDeleted, [&deletedFolders](QString file) - { - deletedFolders.push_back(file.toUtf8().constData()); - }); - - m_assetProcessorManager->AssessDeletedFile(absPath); - ASSERT_TRUE(BlockUntilIdle(5000)); - - ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre("textures/a.txt")); - ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre("textures")); -} void DuplicateProcessTest::SetUp() { diff --git a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h index c532b08016..4e644ea457 100644 --- a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h +++ b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h @@ -37,6 +37,114 @@ public: MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&)); }; +class AssetProcessorManager_Test : public AssetProcessor::AssetProcessorManager +{ +public: + friend class GTEST_TEST_CLASS_NAME_( + AssetProcessorManagerTest, AssetProcessedImpl_DifferentProductDependenciesPerProduct_SavesCorrectlyToDatabase); + + friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies); + friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution); + friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, SameFilenameForAllPlatforms); + + friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath); + + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles); + + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTest); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDifferentTypes_BasicTest); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Reverse_BasicTest); + friend class GTEST_TEST_CLASS_NAME_( + AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_MissingFiles_ReturnsNoPathWithPlaceholders); + + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_BeforeComputingDirtiness_AllDirty); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_EmptyDatabase_AllDirty); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_SameAsLastTime_NoneDirty); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_MoreThanLastTime_NewOneIsDirty); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_FewerThanLastTime_Dirty); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPattern_CountsAsNew); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPatternType_CountsAsNew); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewPattern_CountsAsNewBuilder); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewVersionNumber_IsNotANewBuilder); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewAnalysisFingerprint_IsNotANewBuilder); + + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_BasicTest); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_UpdateTest); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName); + friend class GTEST_TEST_CLASS_NAME_( + AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid_UpdatesWhenTheyAppear); + friend class GTEST_TEST_CLASS_NAME_( + AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName_UpdatesWhenTheyAppear); + friend class GTEST_TEST_CLASS_NAME_( + AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardMissingFiles_ByName_UpdatesWhenTheyAppear); + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, JobDependencyOrderOnce_MultipleJobs_EmitOK); + + friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint); + + friend class GTEST_TEST_CLASS_NAME_( + AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_DoesNotDuplicateDependency); + friend class GTEST_TEST_CLASS_NAME_( + AbsolutePathProductDependencyTest, AbsolutePathProductDependency_RetryDeferredDependenciesWithMatchingSource_DependencyResolves); + friend class GTEST_TEST_CLASS_NAME_( + AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap); + friend class GTEST_TEST_CLASS_NAME_( + AbsolutePathProductDependencyTest, + UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap); + + friend class GTEST_TEST_CLASS_NAME_(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache); + friend class GTEST_TEST_CLASS_NAME_(MetadataFileTest, MetadataFile_SourceFileExtensionDifferentCase); + + friend class AssetProcessorManagerTest; + friend struct JobDependencyTest; + friend struct ChainJobDependencyTest; + friend struct DeleteTest; + friend struct PathDependencyTest; + friend struct DuplicateProductsTest; + friend struct DuplicateProcessTest; + friend struct AbsolutePathProductDependencyTest; + friend struct WildcardSourceDependencyTest; + + explicit AssetProcessorManager_Test(AssetProcessor::PlatformConfiguration* config, QObject* parent = nullptr); + ~AssetProcessorManager_Test() override; + + bool CheckJobKeyToJobRunKeyMap(AZStd::string jobKey); + + int CountDirtyBuilders() const + { + int numDirty = 0; + for (const auto& element : m_builderDataCache) + { + if (element.second.m_isDirty) + { + ++numDirty; + } + } + return numDirty; + } + + bool IsBuilderDirty(const AZ::Uuid& builderBusId) const + { + auto finder = m_builderDataCache.find(builderBusId); + if (finder == m_builderDataCache.end()) + { + return true; + } + return finder->second.m_isDirty; + } + + void RecomputeDirtyBuilders() + { + // Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping + ComputeBuilderDirty(); + ComputeBuilderDirty(); + } + + using AssetProcessorManager::m_stateData; + using AssetProcessorManager::ComputeBuilderDirty; +}; + + class AssetProcessorManagerTest : public AssetProcessor::AssetProcessorTest { @@ -165,33 +273,6 @@ struct MockBuilderInfoHandler int m_createJobsCount = 0; }; -struct ModtimeScanningTest - : public AssetProcessorManagerTest -{ - void SetUp() override; - void TearDown() override; - - void ProcessAssetJobs(); - void SimulateAssetScanner(QSet filePaths); - QSet BuildFileSet(); - void ExpectWork(int createJobs, int processJobs); - void ExpectNoWork(); - void SetFileContents(QString filePath, QString contents); - - struct StaticData - { - QString m_relativePathFromWatchFolder[3]; - AZStd::vector m_absolutePath; - AZStd::vector m_processResults; - AZStd::unordered_multimap m_productPaths; - AZStd::vector m_deletedSources; - AZStd::shared_ptr m_builderTxtBuilder; - MockBuilderInfoHandler m_mockBuilderInfoHandler; - }; - - AZStd::unique_ptr m_data; -}; - struct MetadataFileTest : public AssetProcessorManagerTest @@ -274,9 +355,3 @@ struct DuplicateProductsTest { void SetupDuplicateProductsTest(QString& sourceFile, QDir& tempPath, QString& productFile, AZStd::vector& jobDetails, AssetBuilderSDK::ProcessJobResponse& response, bool multipleOutputs, QString extension); }; - -struct DeleteTest - : public ModtimeScanningTest -{ - void SetUp() override; -}; diff --git a/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp b/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp new file mode 100644 index 0000000000..6b3124ca00 --- /dev/null +++ b/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp @@ -0,0 +1,706 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +namespace UnitTests +{ + using AssetFileInfo = AssetProcessor::AssetFileInfo; + + void ModtimeScanningTest::SetUpAssetProcessorManager() + { + using namespace AssetProcessor; + + m_assetProcessorManager->SetEnableModtimeSkippingFeature(true); + m_assetProcessorManager->RecomputeDirtyBuilders(); + + QObject::connect( + m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, + [this](JobDetails details) + { + m_data->m_processResults.push_back(AZStd::move(details)); + }); + + QObject::connect( + m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted, + [this](QString file) + { + m_data->m_deletedSources.push_back(file); + }); + + m_idleConnection = QObject::connect( + m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState, + [this](bool newState) + { + m_isIdling = newState; + }); + } + + void ModtimeScanningTest::SetUp() + { + using namespace AssetProcessor; + + AssetProcessorManagerTest::SetUp(); + + m_data = AZStd::make_unique(); + + // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own + m_mockApplicationManager->BusDisconnect(); + + m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc( + "test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", + { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) }); + m_data->m_mockBuilderInfoHandler.BusConnect(); + + ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder)); + + SetUpAssetProcessorManager(); + + // Create the test file + const auto& scanFolder = m_config->GetScanFolderAt(0); + m_data->m_relativePathFromWatchFolder[0] = "modtimeTestFile.txt"; + m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[0])); + + m_data->m_relativePathFromWatchFolder[1] = "modtimeTestDependency.txt"; + m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[1])); + + m_data->m_relativePathFromWatchFolder[2] = "modtimeTestDependency.txt.assetinfo"; + m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[2])); + + for (const auto& path : m_data->m_absolutePath) + { + ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path, "")); + } + + m_data->m_mockBuilderInfoHandler.m_dependencyFilePath = m_data->m_absolutePath[1].toUtf8().data(); + + // Add file to database with no modtime + { + AssetDatabaseConnection connection; + ASSERT_TRUE(connection.OpenDatabase()); + AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; + fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[0].toUtf8().data(); + fileEntry.m_modTime = 0; + fileEntry.m_isFolder = false; + fileEntry.m_scanFolderPK = scanFolder.ScanFolderID(); + + bool entryAlreadyExists; + ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); + ASSERT_FALSE(entryAlreadyExists); + + fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry + fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[1].toUtf8().data(); + ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); + ASSERT_FALSE(entryAlreadyExists); + + fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry + fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[2].toUtf8().data(); + ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists)); + ASSERT_FALSE(entryAlreadyExists); + } + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ASSERT_TRUE(BlockUntilIdle(5000)); + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); + ASSERT_EQ(m_data->m_processResults.size(), 2); + ASSERT_EQ(m_data->m_deletedSources.size(), 0); + + ProcessAssetJobs(); + + m_data->m_processResults.clear(); + m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; + + m_isIdling = false; + } + + void ModtimeScanningTest::TearDown() + { + m_data = nullptr; + + AssetProcessorManagerTest::TearDown(); + } + + void ModtimeScanningTest::ProcessAssetJobs() + { + m_data->m_productPaths.clear(); + + for (const auto& processResult : m_data->m_processResults) + { + auto file = + QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1"); + m_data->m_productPaths.emplace( + QDir(processResult.m_jobEntry.m_watchFolderPath) + .absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName) + .toUtf8() + .constData(), + file); + + // Create the file on disk + ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products.")); + + AssetBuilderSDK::ProcessJobResponse response; + response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success; + response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1)); + + using JobEntry = AssetProcessor::JobEntry; + + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry), + Q_ARG(AssetBuilderSDK::ProcessJobResponse, response)); + } + + ASSERT_TRUE(BlockUntilIdle(5000)); + + m_isIdling = false; + } + + void ModtimeScanningTest::SimulateAssetScanner(QSet filePaths) + { + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, + Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started)); + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet, filePaths)); + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, + Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed)); + } + + QSet ModtimeScanningTest::BuildFileSet() + { + QSet filePaths; + + for (const auto& path : m_data->m_absolutePath) + { + QFileInfo fileInfo(path); + auto modtime = fileInfo.lastModified(); + AZ::u64 fileSize = fileInfo.size(); + filePaths.insert(AssetFileInfo(path, modtime, fileSize, m_config->GetScanFolderForFile(path), false)); + } + + return filePaths; + } + + void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs) + { + ASSERT_TRUE(BlockUntilIdle(5000)); + + EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs); + EXPECT_EQ(m_data->m_processResults.size(), processJobs); + for (int i = 0; i < processJobs; ++i) + { + EXPECT_FALSE(m_data->m_processResults[i].m_autoFail); + } + EXPECT_EQ(m_data->m_deletedSources.size(), 0); + + m_isIdling = false; + } + + void ModtimeScanningTest::ExpectNoWork() + { + // Since there's no work to do, the idle event isn't going to trigger, just process events a couple times + for (int i = 0; i < 10; ++i) + { + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + } + + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0); + ASSERT_EQ(m_data->m_processResults.size(), 0); + ASSERT_EQ(m_data->m_deletedSources.size(), 0); + + m_isIdling = false; + } + + void ModtimeScanningTest::SetFileContents(QString filePath, QString contents) + { + QFile file(filePath); + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.write(contents.toUtf8().constData()); + file.close(); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping) + { + using namespace AzToolsFramework::AssetSystem; + + // Make sure modtime skipping is disabled + // We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off + m_assetProcessorManager->SetEnableModtimeSkippingFeature(false); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // 2 create jobs but 0 process jobs because the file has already been processed before in SetUp + ExpectWork(2, 0); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged) + { + using namespace AzToolsFramework::AssetSystem; + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectNoWork(); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform) + { + using namespace AzToolsFramework::AssetSystem; + + AssetUtilities::SetUseFileHashOverride(true, true); + + // Enable android platform after the initial SetUp has already processed the files for pc + QDir tempPath(m_tempDir.path()); + AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" }); + m_config->EnablePlatform(androidPlatform, true); + + // There's no way to remove scanfolders and adding a new one after enabling the platform will cause the pc assets to build as well, + // which we don't want Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder + auto& platforms = const_cast&>(m_config->GetScanFolderAt(0).GetPlatforms()); + platforms.push_back(androidPlatform); + + // We need the builder fingerprints to be updated to reflect the newly enabled platform + m_assetProcessorManager->ComputeBuilderDirty(); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectWork( + 4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed) + + ASSERT_TRUE(m_data->m_processResults[0].m_destinationPath.contains("android")); + ASSERT_TRUE(m_data->m_processResults[1].m_destinationPath.contains("android")); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp) + { + // Update the timestamp on a file without changing its contents + // This should not cause any job to run since the hash of the file is the same before/after + // Additionally, the timestamp stored in the database should be updated + using namespace AzToolsFramework::AssetSystem; + + uint64_t timestamp = 1594923423; + + QString databaseName, scanfolderName; + m_config->ConvertToRelativePath(m_data->m_absolutePath[1], databaseName, scanfolderName); + auto* scanFolder = m_config->GetScanFolderForFile(m_data->m_absolutePath[1]); + + AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; + + m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry); + + ASSERT_NE(fileEntry.m_modTime, timestamp); + uint64_t existingTimestamp = fileEntry.m_modTime; + + // Modify the timestamp on just one file + AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp); + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectNoWork(); + + m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry); + + // The timestamp should be updated even though nothing processed + ASSERT_NE(fileEntry.m_modTime, existingTimestamp); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile) + { + // Update the timestamp on a file without changing its contents + // This should not cause any job to run since the hash of the file is the same before/after + // Additionally, the timestamp stored in the database should be updated + using namespace AzToolsFramework::AssetSystem; + + uint64_t timestamp = 1594923423; + + // Modify the timestamp on just one file + AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp); + + AssetUtilities::SetUseFileHashOverride(true, false); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectWork(2, 2); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile) + { + using namespace AzToolsFramework::AssetSystem; + + SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world"); + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers + // the other test file to process as well + ExpectWork(2, 2); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain) + { + using namespace AzToolsFramework::AssetSystem; + auto theFile = m_data->m_absolutePath[1].toUtf8(); + const char* theFileString = theFile.constData(); + + SetFileContents(theFileString, "hello world"); + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers + // the other test file to process as well + ExpectWork(2, 2); + ProcessAssetJobs(); + + m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; + m_data->m_processResults.clear(); + m_data->m_deletedSources.clear(); + + SetFileContents(theFileString, ""); + + filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // Expect processing to happen again + ExpectWork(2, 2); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess) + { + using namespace AzToolsFramework::AssetSystem; + + SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world"); + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers + // the other test file to process as well + ExpectWork(2, 2); + ProcessAssetJobs(); + + m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; + m_data->m_processResults.clear(); + m_data->m_deletedSources.clear(); + + // Make file 0 have the same contents as file 1 + SetFileContents(m_data->m_absolutePath[0].toUtf8().constData(), "hello world"); + + filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectWork(1, 1); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile) + { + using namespace AzToolsFramework::AssetSystem; + + SetFileContents(m_data->m_absolutePath[2].toUtf8().constData(), "hello world"); + + AssetUtilities::SetUseFileHashOverride(true, true); + + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + // Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file + // that triggers the source file which is a dependency that triggers the other test file to process as well + ExpectWork(2, 2); + } + + TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile) + { + using namespace AzToolsFramework::AssetSystem; + + AssetUtilities::SetUseFileHashOverride(true, true); + + ASSERT_TRUE(QFile::remove(m_data->m_absolutePath[0])); + + // Feed in ONLY one file (the one we didn't delete) + QSet filePaths; + QFileInfo fileInfo(m_data->m_absolutePath[1]); + auto modtime = fileInfo.lastModified(); + AZ::u64 fileSize = fileInfo.size(); + filePaths.insert(AssetFileInfo(m_data->m_absolutePath[1], modtime, fileSize, &m_config->GetScanFolderAt(0), false)); + + SimulateAssetScanner(filePaths); + + QElapsedTimer timer; + timer.start(); + + do + { + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + } while (m_data->m_deletedSources.size() < m_data->m_relativePathFromWatchFolder[0].size() && timer.elapsed() < 5000); + + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0); + ASSERT_EQ(m_data->m_processResults.size(), 0); + ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_relativePathFromWatchFolder[0])); + } + + TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed) + { + using namespace AzToolsFramework::AssetSystem; + + m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]); + + ASSERT_TRUE(BlockUntilIdle(5000)); + + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1); + ASSERT_EQ(m_data->m_processResults.size(), 1); + } + + TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess) + { + using namespace AzToolsFramework::AssetSystem; + + using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry; + + SourceFileDependencyEntry newEntry1; + newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId; + newEntry1.m_builderGuid = AZ::Uuid::CreateRandom(); + newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData(); + newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData(); + newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource; + + m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]); + ASSERT_TRUE(BlockUntilIdle(5000)); + + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1); + ASSERT_EQ(m_data->m_processResults.size(), 1); + + m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[1]); + ASSERT_TRUE(BlockUntilIdle(5000)); + + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3); + ASSERT_EQ(m_data->m_processResults.size(), 3); + } + + TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess) + { + using namespace AzToolsFramework::AssetSystem; + + const auto& scanFolder = m_config->GetScanFolderAt(0); + + QString scanPath = scanFolder.ScanPath(); + m_assetProcessorManager->RequestReprocess(scanPath); + ASSERT_TRUE(BlockUntilIdle(5000)); + + // two text files are source assets, assetinfo is not + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); + ASSERT_EQ(m_data->m_processResults.size(), 2); + } + + void DeleteTest::SetUp() + { + AssetProcessorManagerTest::SetUp(); + + m_data = AZStd::make_unique(); + + // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own + m_mockApplicationManager->BusDisconnect(); + + m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc( + "test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", + { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) }); + m_data->m_mockBuilderInfoHandler.BusConnect(); + + ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder)); + + SetUpAssetProcessorManager(); + + auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file) + { + using namespace AzToolsFramework::AssetDatabase; + + QString watchFolderPath = scanFolder->ScanPath(); + QString absPath(QDir(watchFolderPath).absoluteFilePath(file)); + UnitTestUtils::CreateDummyFile(absPath); + + m_data->m_absolutePath.push_back(absPath); + + AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry; + fileEntry.m_fileName = file.toUtf8().constData(); + fileEntry.m_modTime = 0; + fileEntry.m_isFolder = false; + fileEntry.m_scanFolderPK = scanFolder->ScanFolderID(); + + bool entryAlreadyExists; + ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists)); + ASSERT_FALSE(entryAlreadyExists); + }; + + // Create test files + QDir tempPath(m_tempDir.path()); + const auto* scanFolder1 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder1")); + const auto* scanFolder4 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder4")); + + createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt")); + createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt")); + + // Run the test files through AP all the way to processing stage + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ASSERT_TRUE(BlockUntilIdle(5000)); + ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2); + ASSERT_EQ(m_data->m_processResults.size(), 2); + ASSERT_EQ(m_data->m_deletedSources.size(), 0); + + ProcessAssetJobs(); + + m_data->m_processResults.clear(); + m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0; + + // Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM + m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get())); + + SetUpAssetProcessorManager(); + } + + TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache) + { + // There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed + // As a result, deleting a folder didn't count as a "folder", so the wrong code path was taken. This test makes sure the correct + // deletion events fire + + using namespace AzToolsFramework::AssetSystem; + + // Feed in the files from the asset scanner, no jobs should run since they're already up-to-date + QSet filePaths = BuildFileSet(); + SimulateAssetScanner(filePaths); + + ExpectNoWork(); + + // Delete one of the folders + QDir tempPath(m_tempDir.path()); + QString absPath(tempPath.absoluteFilePath("subfolder1/textures")); + QDir(absPath).removeRecursively(); + + AZStd::vector deletedFolders; + QObject::connect( + m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::SourceFolderDeleted, + [&deletedFolders](QString file) + { + deletedFolders.push_back(file.toUtf8().constData()); + }); + + m_assetProcessorManager->AssessDeletedFile(absPath); + ASSERT_TRUE(BlockUntilIdle(5000)); + + ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre("textures/a.txt")); + ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre("textures")); + } + + TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails) + { + auto theFile = m_data->m_absolutePath[1].toUtf8(); + const char* theFileString = theFile.constData(); + auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString); + + { + QFile file(theFileString); + file.remove(); + } + + ASSERT_GT(m_data->m_productPaths.size(), 0); + QFile product(productPath); + + ASSERT_TRUE(product.open(QIODevice::ReadOnly)); + + // Check if we can delete the file now, if we can't, proceed with the test + // If we can, it means the OS running this test doesn't lock open files so there's nothing to test + if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData())) + { + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString))); + + EXPECT_TRUE(BlockUntilIdle(5000)); + + EXPECT_TRUE(QFile::exists(productPath)); + EXPECT_EQ(m_data->m_deletedSources.size(), 0); + } + else + { + SUCCEED() << "Skipping test. OS does not lock open files."; + } + } + + TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased) + { + // This test is intended to verify the AP will successfully retry deleting a source asset + // when one of its product assets is locked temporarily + // We'll lock the file by holding it open + + auto theFile = m_data->m_absolutePath[1].toUtf8(); + const char* theFileString = theFile.constData(); + auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString); + + { + QFile file(theFileString); + file.remove(); + } + + ASSERT_GT(m_data->m_productPaths.size(), 0); + QFile product(productPath); + + // Open the file and keep it open to lock it + // We'll start a thread later to unlock the file + // This will allow us to test how AP handles trying to delete a locked file + ASSERT_TRUE(product.open(QIODevice::ReadOnly)); + + // Check if we can delete the file now, if we can't, proceed with the test + // If we can, it means the OS running this test doesn't lock open files so there's nothing to test + if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData())) + { + m_deleteCounter = 0; + + // Set up a callback which will fire after at least 1 retry + // Unlock the file at that point so AP can successfully delete it + m_callback = [&product]() + { + product.close(); + }; + + QMetaObject::invokeMethod( + m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString))); + + EXPECT_TRUE(BlockUntilIdle(5000)); + + EXPECT_FALSE(QFile::exists(productPath)); + EXPECT_EQ(m_data->m_deletedSources.size(), 1); + + EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file + m_errorAbsorber->ExpectAsserts(0); + } + else + { + SUCCEED() << "Skipping test. OS does not lock open files."; + } + } +} diff --git a/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.h b/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.h new file mode 100644 index 0000000000..ef57c70536 --- /dev/null +++ b/Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace UnitTests +{ + struct ModtimeScanningTest : AssetProcessorManagerTest + { + void SetUpAssetProcessorManager(); + void SetUp() override; + void TearDown() override; + + void ProcessAssetJobs(); + void SimulateAssetScanner(QSet filePaths); + QSet BuildFileSet(); + void ExpectWork(int createJobs, int processJobs); + void ExpectNoWork(); + void SetFileContents(QString filePath, QString contents); + + struct StaticData + { + QString m_relativePathFromWatchFolder[3]; + AZStd::vector m_absolutePath; + AZStd::vector m_processResults; + AZStd::unordered_multimap m_productPaths; + AZStd::vector m_deletedSources; + AZStd::shared_ptr m_builderTxtBuilder; + MockBuilderInfoHandler m_mockBuilderInfoHandler; + }; + + AZStd::unique_ptr m_data; + }; + + struct DeleteTest : ModtimeScanningTest + { + void SetUp() override; + }; + + + struct LockedFileTest + : ModtimeScanningTest + , AssetProcessor::ConnectionBus::Handler + { + MOCK_METHOD3(SendRaw, size_t(unsigned, unsigned, const QByteArray&)); + MOCK_METHOD3(SendPerPlatform, size_t(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const QString&)); + MOCK_METHOD4(SendRawPerPlatform, size_t(unsigned, unsigned, const QByteArray&, const QString&)); + MOCK_METHOD2(SendRequest, unsigned(const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const ResponseCallback&)); + MOCK_METHOD2(SendResponse, size_t(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&)); + MOCK_METHOD1(RemoveResponseHandler, void(unsigned)); + + size_t Send(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override + { + using SourceFileNotificationMessage = AzToolsFramework::AssetSystem::SourceFileNotificationMessage; + switch (message.GetMessageType()) + { + case SourceFileNotificationMessage::MessageType: + if (const auto sourceFileMessage = azrtti_cast(&message); + sourceFileMessage != nullptr && + sourceFileMessage->m_type == SourceFileNotificationMessage::NotificationType::FileRemoved) + { + // The File Remove message will occur before an attempt to delete the file + // Wait for more than 1 File Remove message. + // This indicates the AP has attempted to delete the file once, failed to do so and is now retrying + ++m_deleteCounter; + + if (m_deleteCounter > 1 && m_callback) + { + m_callback(); + m_callback = {}; // Unset it to be safe, we only intend to run the callback once + } + } + break; + default: + break; + } + + return 0; + } + + void SetUp() override + { + ModtimeScanningTest::SetUp(); + + AssetProcessor::ConnectionBus::Handler::BusConnect(0); + } + + void TearDown() override + { + AssetProcessor::ConnectionBus::Handler::BusDisconnect(); + + ModtimeScanningTest::TearDown(); + } + + AZStd::atomic_int m_deleteCounter{ 0 }; + AZStd::function m_callback; + }; +} diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 3d100ec170..b2217435b2 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -500,6 +500,10 @@ QProgressBar::chunk { /************** Gem Catalog **************/ +#GemCatalogScreen { + background-color: #333333; +} + #GemCatalogTitle { font-size: 18px; } @@ -546,9 +550,8 @@ QProgressBar::chunk { min-height:24px; } -#GemCatalogHeaderLabel { - font-size: 12px; - color: #FFFFFF; +#adjustableHeaderWidget QHeaderView::section { + background-color: transparent; } #GemCatalogHeaderShowCountLabel { @@ -732,15 +735,6 @@ QProgressBar::chunk { stop: 0 #555555, stop: 1.0 #777777); } -#gemRepoHeaderTable { - background-color: transparent; - max-height: 30px; -} - -#gemRepoListHeader { - background-color: transparent; -} - #gemRepoInspector { background: #444444; } @@ -774,4 +768,4 @@ QProgressBar::chunk { #gemRepoInspectorAddInfoTitleLabel { font-size: 16px; color: #FFFFFF; -} \ No newline at end of file +} diff --git a/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.cpp new file mode 100644 index 0000000000..2b4732a168 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include + +namespace O3DE::ProjectManager +{ + AdjustableHeaderWidget::AdjustableHeaderWidget( const QStringList& headerLabels, + const QVector& defaultHeaderWidths, int minHeaderWidth, + const QVector& resizeModes, QWidget* parent) + : QTableWidget(parent) + { + setObjectName("adjustableHeaderWidget"); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + setFixedHeight(s_headerWidgetHeight); + + m_header = horizontalHeader(); + m_header->setDefaultAlignment(Qt::AlignLeft); + + setColumnCount(headerLabels.count()); + setHorizontalHeaderLabels(headerLabels); + + AZ_Assert(defaultHeaderWidths.count() == columnCount(), "Default header widths does not match number of columns"); + AZ_Assert(resizeModes.count() == columnCount(), "Resize modesdoes not match number of columns"); + + for (int column = 0; column < columnCount(); ++column) + { + m_header->resizeSection(column, defaultHeaderWidths[column]); + m_header->setSectionResizeMode(column, resizeModes[column]); + } + + m_header->setMinimumSectionSize(minHeaderWidth); + m_header->setCascadingSectionResizes(true); + + connect(m_header, &QHeaderView::sectionResized, this, &AdjustableHeaderWidget::OnSectionResized); + } + + void AdjustableHeaderWidget::OnSectionResized(int logicalIndex, int oldSize, int newSize) + { + const int headerCount = columnCount(); + const int headerWidth = m_header->width(); + const int totalSectionWidth = m_header->length(); + + if (totalSectionWidth > headerWidth && newSize > oldSize) + { + int xPos = 0; + int requiredWidth = 0; + + for (int i = 0; i < headerCount; i++) + { + if (i < logicalIndex) + { + xPos += m_header->sectionSize(i); + } + else if (i == logicalIndex) + { + xPos += newSize; + } + else if (i > logicalIndex) + { + if (m_header->sectionResizeMode(i) == QHeaderView::ResizeMode::Fixed) + { + requiredWidth += m_header->sectionSize(i); + } + else + { + requiredWidth += m_header->minimumSectionSize(); + } + } + } + + if (xPos + requiredWidth > headerWidth) + { + m_header->resizeSection(logicalIndex, oldSize); + } + } + + // wait till all columns resized + QTimer::singleShot(0, [&]() + { + // only re-paint when the header and section widths have settled + const int headerWidth = m_header->width(); + const int totalSectionWidth = m_header->length(); + if (totalSectionWidth == headerWidth) + { + emit sectionsResized(); + } + }); + } + + QPair AdjustableHeaderWidget::CalcColumnXBounds(int headerIndex) const + { + // Total the widths of all headers before this one in first and including it in second + QPair bounds(0, 0); + + for (int curIndex = 0; curIndex <= headerIndex; ++curIndex) + { + if (curIndex == headerIndex) + { + bounds.first = bounds.second; + } + bounds.second += m_header->sectionSize(curIndex); + } + + return bounds; + } + + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.h b/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.h new file mode 100644 index 0000000000..26a5ce13c7 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/AdjustableHeaderWidget.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include + +#include +#include +#include +#include +#endif + +namespace O3DE::ProjectManager +{ + // Using a QTableWidget for its header + // Using a seperate model allows the setup of a header exactly as needed + class AdjustableHeaderWidget + : public QTableWidget + { + Q_OBJECT + + public: + explicit AdjustableHeaderWidget(const QStringList& headerLabels, + const QVector& defaultHeaderWidths, int minHeaderWidth, + const QVector& resizeModes, + QWidget* parent = nullptr); + ~AdjustableHeaderWidget() = default; + + QPair CalcColumnXBounds(int headerIndex) const; + + inline constexpr static int s_headerTextIndent = 7; + inline constexpr static int s_headerWidgetHeight = 24; + + QHeaderView* m_header; + + signals: + void sectionsResized(); + + protected slots: + void OnSectionResized(int logicalIndex, int oldSize, int newSize); + + private: + inline constexpr static int s_headerIndentSection = 11; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ExternalLinkDialog.h b/Code/Tools/ProjectManager/Source/ExternalLinkDialog.h index 45d391e64e..f1b6574b67 100644 --- a/Code/Tools/ProjectManager/Source/ExternalLinkDialog.h +++ b/Code/Tools/ProjectManager/Source/ExternalLinkDialog.h @@ -17,7 +17,7 @@ namespace O3DE::ProjectManager class ExternalLinkDialog : public QDialog { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit ExternalLinkDialog(const QUrl& url, QWidget* parent = nullptr); ~ExternalLinkDialog() = default; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index b749e9831d..4e945f5c99 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -33,7 +33,7 @@ namespace O3DE::ProjectManager class GemCartWidget : public QScrollArea { - Q_OBJECT // AUTOMOC + Q_OBJECT public: GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 5bc0b26ca5..ebabff45cc 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -40,6 +42,13 @@ namespace O3DE::ProjectManager GemCatalogScreen::GemCatalogScreen(QWidget* parent) : ScreenWidget(parent) { + // The width of either side panel (filters, inspector) in the catalog + constexpr int sidePanelWidth = 240; + // Querying qApp about styling reports the scroll bar being larger than it is so define it manually + constexpr int verticalScrollBarWidth = 8; + + setObjectName("GemCatalogScreen"); + m_gemModel = new GemModel(this); m_proxyModel = new GemSortFilterProxyModel(m_gemModel, this); @@ -69,10 +78,8 @@ namespace O3DE::ProjectManager hLayout->setMargin(0); vLayout->addLayout(hLayout); - m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), this); - m_rightPanelStack = new QStackedWidget(this); - m_rightPanelStack->setFixedWidth(240); + m_rightPanelStack->setFixedWidth(sidePanelWidth); m_gemInspector = new GemInspector(m_gemModel, this); @@ -81,18 +88,45 @@ namespace O3DE::ProjectManager connect(m_gemInspector, &GemInspector::UninstallGem, this, &GemCatalogScreen::UninstallGem); QWidget* filterWidget = new QWidget(this); - filterWidget->setFixedWidth(240); + filterWidget->setFixedWidth(sidePanelWidth); m_filterWidgetLayout = new QVBoxLayout(); m_filterWidgetLayout->setMargin(0); m_filterWidgetLayout->setSpacing(0); filterWidget->setLayout(m_filterWidgetLayout); - GemListHeaderWidget* listHeaderWidget = new GemListHeaderWidget(m_proxyModel); + GemListHeaderWidget* catalogHeaderWidget = new GemListHeaderWidget(m_proxyModel); + + constexpr int minHeaderSectionWidth = 100; + AdjustableHeaderWidget* listHeaderWidget = new AdjustableHeaderWidget( + QStringList{ tr("Gem Name"), tr("Gem Summary"), tr("Status") }, + QVector{ + GemItemDelegate::s_defaultSummaryStartX - 30, + 0, // Section is set to stretch to fit + GemItemDelegate::s_buttonWidth + GemItemDelegate::s_itemMargins.left() + GemItemDelegate::s_itemMargins.right() + GemItemDelegate::s_contentMargins.right() + }, + minHeaderSectionWidth, + QVector + { + QHeaderView::ResizeMode::Interactive, + QHeaderView::ResizeMode::Stretch, + QHeaderView::ResizeMode::Fixed + }, + this); + + m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), listHeaderWidget, this); + + QHBoxLayout* listHeaderLayout = new QHBoxLayout(); + listHeaderLayout->setMargin(0); + listHeaderLayout->setSpacing(0); + listHeaderLayout->addSpacing(GemItemDelegate::s_itemMargins.left()); + listHeaderLayout->addWidget(listHeaderWidget); + listHeaderLayout->addSpacing(GemItemDelegate::s_itemMargins.right() + verticalScrollBarWidth); QVBoxLayout* middleVLayout = new QVBoxLayout(); middleVLayout->setMargin(0); middleVLayout->setSpacing(0); - middleVLayout->addWidget(listHeaderWidget); + middleVLayout->addWidget(catalogHeaderWidget); + middleVLayout->addLayout(listHeaderLayout); middleVLayout->addWidget(m_gemListView); hLayout->addWidget(filterWidget); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemDependenciesDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemDependenciesDialog.h index df8ca6f8a2..1858637aa5 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemDependenciesDialog.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemDependenciesDialog.h @@ -19,7 +19,7 @@ namespace O3DE::ProjectManager class GemDependenciesDialog : public QDialog { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemDependenciesDialog(GemModel* gemModel, QWidget *parent = nullptr); ~GemDependenciesDialog() = default; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h index e422178d08..729a63af64 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h @@ -26,7 +26,7 @@ namespace O3DE::ProjectManager class FilterCategoryWidget : public QWidget { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit FilterCategoryWidget(const QString& header, diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h index 1713191623..c71d43eaac 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h @@ -28,7 +28,7 @@ namespace O3DE::ProjectManager class GemInspector : public QScrollArea { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemInspector(GemModel* model, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp index dd94e42fc4..1733257e3b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp @@ -9,6 +9,8 @@ #include #include #include +#include + #include #include @@ -22,12 +24,14 @@ #include #include #include +#include namespace O3DE::ProjectManager { - GemItemDelegate::GemItemDelegate(QAbstractItemModel* model, QObject* parent) + GemItemDelegate::GemItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent) : QStyledItemDelegate(parent) , m_model(model) + , m_headerWidget(header) { AddPlatformIcon(GemInfo::Android, ":/Android.svg"); AddPlatformIcon(GemInfo::iOS, ":/iOS.svg"); @@ -116,12 +120,15 @@ namespace O3DE::ProjectManager // Gem name QString gemName = GemModel::GetDisplayName(modelIndex); QFont gemNameFont(options.font); - const int firstColumnMaxTextWidth = s_summaryStartX - 30; + QPair nameXBounds = CalcColumnXBounds(HeaderOrder::Name); + const int nameStartX = nameXBounds.first; + const int firstColumnTextStartX = s_itemMargins.left() + nameStartX + AdjustableHeaderWidget::s_headerTextIndent; + const int firstColumnMaxTextWidth = nameXBounds.second - nameStartX - AdjustableHeaderWidget::s_headerTextIndent; gemNameFont.setPixelSize(static_cast(s_gemNameFontSize)); gemNameFont.setBold(true); gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth); QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize); - gemNameRect.moveTo(contentRect.left(), contentRect.top()); + gemNameRect.moveTo(firstColumnTextStartX, contentRect.top()); painter->setFont(gemNameFont); painter->setPen(m_textColor); gemNameRect = painter->boundingRect(gemNameRect, Qt::TextSingleLine, gemName); @@ -131,7 +138,7 @@ namespace O3DE::ProjectManager QString gemCreator = GemModel::GetCreator(modelIndex); gemCreator = standardFontMetrics.elidedText(gemCreator, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth); QRect gemCreatorRect = GetTextRect(standardFont, gemCreator, s_fontSize); - gemCreatorRect.moveTo(contentRect.left(), contentRect.top() + gemNameRect.height()); + gemCreatorRect.moveTo(firstColumnTextStartX, contentRect.top() + gemNameRect.height()); painter->setFont(standardFont); gemCreatorRect = painter->boundingRect(gemCreatorRect, Qt::TextSingleLine, gemCreator); @@ -157,10 +164,13 @@ namespace O3DE::ProjectManager const int featureTagAreaHeight = 30; const int summaryHeight = contentRect.height() - (hasTags * featureTagAreaHeight); - const int additionalSummarySpacing = s_itemMargins.right() * 3; - const QSize summarySize = QSize(contentRect.width() - s_summaryStartX - s_buttonWidth - additionalSummarySpacing, + const auto [summaryStartX, summaryEndX] = CalcColumnXBounds(HeaderOrder::Summary); + + const QSize summarySize = + QSize(summaryEndX - summaryStartX - AdjustableHeaderWidget::s_headerTextIndent - s_extraSummarySpacing, summaryHeight); - return QRect(QPoint(contentRect.left() + s_summaryStartX, contentRect.top()), summarySize); + return QRect( + QPoint(s_itemMargins.left() + summaryStartX + AdjustableHeaderWidget::s_headerTextIndent, contentRect.top()), summarySize); } QSize GemItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const @@ -169,7 +179,7 @@ namespace O3DE::ProjectManager initStyleOption(&options, modelIndex); int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right(); - return QSize(marginsHorizontal + s_buttonWidth + s_summaryStartX, s_height); + return QSize(marginsHorizontal + s_buttonWidth + s_defaultSummaryStartX, s_height); } bool GemItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) @@ -299,9 +309,17 @@ namespace O3DE::ProjectManager return QFontMetrics(font).boundingRect(text); } + QPair GemItemDelegate::CalcColumnXBounds(HeaderOrder header) const + { + return m_headerWidget->CalcColumnXBounds(static_cast(header)); + } + QRect GemItemDelegate::CalcButtonRect(const QRect& contentRect) const { - const QPoint topLeft = QPoint(contentRect.right() - s_buttonWidth, contentRect.center().y() - s_buttonHeight / 2); + const QPoint topLeft = QPoint( + s_itemMargins.left() + CalcColumnXBounds(HeaderOrder::Status).first + AdjustableHeaderWidget::s_headerTextIndent + s_statusIconSize + + s_statusButtonSpacing, + contentRect.center().y() - s_buttonHeight / 2); const QSize size = QSize(s_buttonWidth, s_buttonHeight); return QRect(topLeft, size); } @@ -331,18 +349,23 @@ namespace O3DE::ProjectManager } } - void GemItemDelegate::DrawFeatureTags(QPainter* painter, const QRect& contentRect, const QStringList& featureTags, const QFont& standardFont, const QRect& summaryRect) const + void GemItemDelegate::DrawFeatureTags( + QPainter* painter, + const QRect& contentRect, + const QStringList& featureTags, + const QFont& standardFont, + const QRect& summaryRect) const { QFont gemFeatureTagFont(standardFont); gemFeatureTagFont.setPixelSize(s_featureTagFontSize); gemFeatureTagFont.setBold(false); painter->setFont(gemFeatureTagFont); - int x = s_summaryStartX; + int x = CalcColumnXBounds(HeaderOrder::Summary).first + AdjustableHeaderWidget::s_headerTextIndent; for (const QString& featureTag : featureTags) { QRect featureTagRect = GetTextRect(gemFeatureTagFont, featureTag, s_featureTagFontSize); - featureTagRect.moveTo(contentRect.left() + x + s_featureTagBorderMarginX, + featureTagRect.moveTo(s_itemMargins.left() + x + s_featureTagBorderMarginX, contentRect.top() + 47); featureTagRect = painter->boundingRect(featureTagRect, Qt::TextSingleLine, featureTag); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h index 107de6de15..a08fcb0a4b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h @@ -19,13 +19,15 @@ QT_FORWARD_DECLARE_CLASS(QEvent) namespace O3DE::ProjectManager { + QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget) + class GemItemDelegate : public QStyledItemDelegate { - Q_OBJECT // AUTOMOC + Q_OBJECT public: - explicit GemItemDelegate(QAbstractItemModel* model, QObject* parent = nullptr); + explicit GemItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent = nullptr); ~GemItemDelegate() = default; void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override; @@ -45,12 +47,13 @@ namespace O3DE::ProjectManager inline constexpr static int s_height = 105; // Gem item total height inline constexpr static qreal s_gemNameFontSize = 13.0; inline constexpr static qreal s_fontSize = 12.0; - inline constexpr static int s_summaryStartX = 150; + inline constexpr static int s_defaultSummaryStartX = 190; // Margin and borders inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/16, /*top=*/8, /*right=*/16, /*bottom=*/8); // Item border distances inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/12, /*right=*/20, /*bottom=*/12); // Distances of the elements within an item to the item borders inline constexpr static int s_borderWidth = 4; + inline constexpr static int s_extraSummarySpacing = s_itemMargins.right(); // Button inline constexpr static int s_buttonWidth = 32; @@ -65,6 +68,13 @@ namespace O3DE::ProjectManager inline constexpr static int s_featureTagBorderMarginY = 3; inline constexpr static int s_featureTagSpacing = 7; + enum class HeaderOrder + { + Name, + Summary, + Status + }; + signals: void MovieStartedPlaying(const QMovie* playingMovie) const; @@ -74,13 +84,20 @@ namespace O3DE::ProjectManager void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const; QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const; + QPair CalcColumnXBounds(HeaderOrder header) const; QRect CalcButtonRect(const QRect& contentRect) const; QRect CalcSummaryRect(const QRect& contentRect, bool hasTags) const; void DrawPlatformIcons(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const; void DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const; - void DrawFeatureTags(QPainter* painter, const QRect& contentRect, const QStringList& featureTags, const QFont& standardFont, const QRect& summaryRect) const; + void DrawFeatureTags( + QPainter* painter, + const QRect& contentRect, + const QStringList& featureTags, + const QFont& standardFont, + const QRect& summaryRect) const; void DrawText(const QString& text, QPainter* painter, const QRect& rect, const QFont& standardFont) const; - void DrawDownloadStatusIcon(QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const; + void DrawDownloadStatusIcon( + QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const; QAbstractItemModel* m_model = nullptr; @@ -100,5 +117,7 @@ namespace O3DE::ProjectManager QPixmap m_downloadSuccessfulPixmap; QPixmap m_downloadFailedPixmap; QMovie* m_downloadingMovie = nullptr; + + AdjustableHeaderWidget* m_headerWidget = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp index 10ff31f33b..ec54b413b6 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp @@ -78,37 +78,9 @@ namespace O3DE::ProjectManager // Separating line QFrame* hLine = new QFrame(); hLine->setFrameShape(QFrame::HLine); - hLine->setStyleSheet("color: #666666;"); + hLine->setObjectName("horizontalSeparatingLine"); vLayout->addWidget(hLine); vLayout->addSpacing(GemItemDelegate::s_contentMargins.top()); - - // Bottom section - QHBoxLayout* columnHeaderLayout = new QHBoxLayout(); - columnHeaderLayout->setAlignment(Qt::AlignLeft); - - const int gemNameStartX = GemItemDelegate::s_itemMargins.left() + GemItemDelegate::s_contentMargins.left() - 1; - columnHeaderLayout->addSpacing(gemNameStartX); - - QLabel* gemNameLabel = new QLabel(tr("Gem Name")); - gemNameLabel->setObjectName("GemCatalogHeaderLabel"); - columnHeaderLayout->addWidget(gemNameLabel); - - columnHeaderLayout->addSpacing(89); - - QLabel* gemSummaryLabel = new QLabel(tr("Gem Summary")); - gemSummaryLabel->setObjectName("GemCatalogHeaderLabel"); - columnHeaderLayout->addWidget(gemSummaryLabel); - - QSpacerItem* horizontalSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); - columnHeaderLayout->addSpacerItem(horizontalSpacer); - - QLabel* gemSelectedLabel = new QLabel(tr("Status")); - gemSelectedLabel->setObjectName("GemCatalogHeaderLabel"); - columnHeaderLayout->addWidget(gemSelectedLabel); - - columnHeaderLayout->addSpacing(72); - - vLayout->addLayout(columnHeaderLayout); } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.h index 537748c849..350c17bbf9 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.h @@ -19,7 +19,7 @@ namespace O3DE::ProjectManager class GemListHeaderWidget : public QFrame { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemListHeaderWidget(GemSortFilterProxyModel* proxyModel, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp index cfdf7fa5b3..d68cbf511b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp @@ -8,12 +8,15 @@ #include #include +#include #include +#include namespace O3DE::ProjectManager { - GemListView::GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent) + GemListView::GemListView( + QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent) : QListView(parent) { setObjectName("GemCatalogListView"); @@ -21,7 +24,7 @@ namespace O3DE::ProjectManager setModel(model); setSelectionModel(selectionModel); - GemItemDelegate* itemDelegate = new GemItemDelegate(model, this); + GemItemDelegate* itemDelegate = new GemItemDelegate(model, header, this); connect(itemDelegate, &GemItemDelegate::MovieStartedPlaying, [=](const QMovie* playingMovie) { @@ -31,6 +34,8 @@ namespace O3DE::ProjectManager this->viewport()->repaint(); }); }); + + connect(header, &AdjustableHeaderWidget::sectionsResized, [=] { update(); }); setItemDelegate(itemDelegate); } diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h index b1b0e0c077..81f5255d9b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h @@ -16,13 +16,15 @@ namespace O3DE::ProjectManager { + QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget) + class GemListView : public QListView { - Q_OBJECT // AUTOMOC + Q_OBJECT public: - explicit GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr); + explicit GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent = nullptr); ~GemListView() = default; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index cb99581468..50f406c97d 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -21,7 +21,7 @@ namespace O3DE::ProjectManager class GemModel : public QStandardItemModel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemModel(QObject* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp index f4a7148d46..ae4bad8901 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp @@ -16,7 +16,7 @@ namespace O3DE::ProjectManager { GemRequirementDelegate::GemRequirementDelegate(QAbstractItemModel* model, QObject* parent) - : GemItemDelegate(model, parent) + : GemItemDelegate(model, nullptr, parent) { } @@ -54,7 +54,7 @@ namespace O3DE::ProjectManager // Gem name QString gemName = GemModel::GetDisplayName(modelIndex); QFont gemNameFont(options.font); - const int firstColumnMaxTextWidth = s_summaryStartX - 30; + const int firstColumnMaxTextWidth = s_defaultSummaryStartX - 30; gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth); gemNameFont.setPixelSize(static_cast(s_gemNameFontSize)); gemNameFont.setBold(true); @@ -75,8 +75,8 @@ namespace O3DE::ProjectManager QRect GemRequirementDelegate::CalcRequirementRect(const QRect& contentRect) const { - const QSize requirementSize = QSize(contentRect.width() - s_summaryStartX - s_itemMargins.right(), contentRect.height()); - return QRect(QPoint(contentRect.left() + s_summaryStartX, contentRect.top()), requirementSize); + const QSize requirementSize = QSize(contentRect.width() - s_defaultSummaryStartX - s_itemMargins.right(), contentRect.height()); + return QRect(QPoint(contentRect.left() + s_defaultSummaryStartX, contentRect.top()), requirementSize); } bool GemRequirementDelegate::editorEvent( diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h index e9001df7fa..cbfb6b1838 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h @@ -18,7 +18,7 @@ namespace O3DE::ProjectManager class GemRequirementDelegate : public GemItemDelegate { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemRequirementDelegate(QAbstractItemModel* model, QObject* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h index af8b1e2cc9..c1dff70ca2 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h @@ -19,7 +19,7 @@ namespace O3DE::ProjectManager class GemRequirementDialog : public QDialog { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemRequirementDialog(GemModel* model, QWidget *parent = nullptr); ~GemRequirementDialog() = default; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h index 7df75f7d94..c527df2ec1 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h @@ -22,7 +22,7 @@ namespace O3DE::ProjectManager class GemRequirementFilterProxyModel : public QSortFilterProxyModel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: GemRequirementFilterProxyModel(GemModel* sourceModel, QObject* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h index 1638fe3fc5..61b3356e06 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h @@ -19,7 +19,7 @@ namespace O3DE::ProjectManager class GemRequirementListView : public QListView { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemRequirementListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h index 0c58d66ccf..6bdeaf828b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h @@ -22,7 +22,7 @@ namespace O3DE::ProjectManager class GemSortFilterProxyModel : public QSortFilterProxyModel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: enum class GemSelected diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h index 9e3f4c3f3b..391d247f90 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h @@ -17,7 +17,7 @@ namespace O3DE::ProjectManager class GemUninstallDialog : public QDialog { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemUninstallDialog(const QString& gemName, QWidget *parent = nullptr); ~GemUninstallDialog() = default; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h index cf34abfb3d..a0996216fd 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h @@ -17,7 +17,7 @@ namespace O3DE::ProjectManager class GemUpdateDialog : public QDialog { - Q_OBJECT // AUTOMOC + Q_OBJECT public : explicit GemUpdateDialog(const QString& gemName, bool updateAvaliable = true, QWidget* parent = nullptr); ~GemUpdateDialog() = default; diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.h index a14472e6a6..f7051d1704 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.h +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.h @@ -26,7 +26,7 @@ namespace O3DE::ProjectManager { class GemRepoInspector : public QScrollArea { - Q_OBJECT // AUTOMOC + Q_OBJECT public : explicit GemRepoInspector(GemRepoModel* model, QWidget* parent = nullptr); ~GemRepoInspector() = default; diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp index fdfcf02155..672cd509d3 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp @@ -9,16 +9,19 @@ #include #include #include +#include #include #include #include +#include namespace O3DE::ProjectManager { - GemRepoItemDelegate::GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent) + GemRepoItemDelegate::GemRepoItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent) : QStyledItemDelegate(parent) , m_model(model) + , m_headerWidget(header) { m_refreshIcon = QIcon(":/Refresh.svg").pixmap(s_refreshIconSize, s_refreshIconSize); m_editIcon = QIcon(":/Edit.svg").pixmap(s_iconSize, s_iconSize); @@ -69,44 +72,55 @@ namespace O3DE::ProjectManager painter->restore(); } + int currentHorizontalOffset = CalcColumnXBounds(HeaderOrder::Name).first; + // Repo name QString repoName = GemRepoModel::GetName(modelIndex); - repoName = QFontMetrics(standardFont).elidedText(repoName, Qt::TextElideMode::ElideRight, s_nameMaxWidth); + int sectionSize = m_headerWidget->m_header->sectionSize(static_cast(HeaderOrder::Name)); + repoName = standardFontMetrics.elidedText(repoName, Qt::TextElideMode::ElideRight, + sectionSize - AdjustableHeaderWidget::s_headerTextIndent); QRect repoNameRect = GetTextRect(standardFont, repoName, s_fontSize); - int currentHorizontalOffset = contentRect.left(); - repoNameRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoNameRect.height() / 2); + repoNameRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent, + contentRect.center().y() - repoNameRect.height() / 2); repoNameRect = painter->boundingRect(repoNameRect, Qt::TextSingleLine, repoName); painter->drawText(repoNameRect, Qt::TextSingleLine, repoName); // Rem repo creator + currentHorizontalOffset += sectionSize; + sectionSize = m_headerWidget->m_header->sectionSize(static_cast(HeaderOrder::Creator)); + QString repoCreator = GemRepoModel::GetCreator(modelIndex); - repoCreator = standardFontMetrics.elidedText(repoCreator, Qt::TextElideMode::ElideRight, s_creatorMaxWidth); + repoCreator = standardFontMetrics.elidedText(repoCreator, Qt::TextElideMode::ElideRight, + sectionSize - AdjustableHeaderWidget::s_headerTextIndent); QRect repoCreatorRect = GetTextRect(standardFont, repoCreator, s_fontSize); - currentHorizontalOffset += s_nameMaxWidth + s_contentSpacing; - repoCreatorRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoCreatorRect.height() / 2); + repoCreatorRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent, + contentRect.center().y() - repoCreatorRect.height() / 2); repoCreatorRect = painter->boundingRect(repoCreatorRect, Qt::TextSingleLine, repoCreator); painter->drawText(repoCreatorRect, Qt::TextSingleLine, repoCreator); // Repo update + currentHorizontalOffset += sectionSize; + sectionSize = m_headerWidget->m_header->sectionSize(static_cast(HeaderOrder::Update)); + QString repoUpdatedDate = GemRepoModel::GetLastUpdated(modelIndex).toString(RepoTimeFormat); - repoUpdatedDate = standardFontMetrics.elidedText(repoUpdatedDate, Qt::TextElideMode::ElideRight, s_updatedMaxWidth); + repoUpdatedDate = standardFontMetrics.elidedText( + repoUpdatedDate, Qt::TextElideMode::ElideRight, + sectionSize - GemRepoItemDelegate::s_refreshIconSpacing - GemRepoItemDelegate::s_refreshIconSize - AdjustableHeaderWidget::s_headerTextIndent); QRect repoUpdatedDateRect = GetTextRect(standardFont, repoUpdatedDate, s_fontSize); - currentHorizontalOffset += s_creatorMaxWidth + s_contentSpacing; - repoUpdatedDateRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoUpdatedDateRect.height() / 2); + repoUpdatedDateRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent, + contentRect.center().y() - repoUpdatedDateRect.height() / 2); repoUpdatedDateRect = painter->boundingRect(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate); painter->drawText(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate); // Draw refresh button - painter->drawPixmap( - repoUpdatedDateRect.left() + s_updatedMaxWidth + s_refreshIconSpacing, - contentRect.center().y() - s_refreshIconSize / 3, // Dividing size by 3 centers much better - m_refreshIcon); + const QRect refreshButtonRect = CalcRefreshButtonRect(contentRect); + painter->drawPixmap(refreshButtonRect.topLeft(), m_refreshIcon); if (options.state & QStyle::State_MouseOver) { @@ -121,8 +135,8 @@ namespace O3DE::ProjectManager QStyleOptionViewItem options(option); initStyleOption(&options, modelIndex); - int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right(); - return QSize(marginsHorizontal + s_nameMaxWidth + s_creatorMaxWidth + s_updatedMaxWidth + s_contentSpacing * 3, s_height); + const int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right(); + return QSize(marginsHorizontal + s_nameDefaultWidth + s_creatorDefaultWidth + s_updatedDefaultWidth, s_height); } bool GemRepoItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) @@ -185,22 +199,31 @@ namespace O3DE::ProjectManager return QFontMetrics(font).boundingRect(text); } + QPair GemRepoItemDelegate::CalcColumnXBounds(HeaderOrder header) const + { + return m_headerWidget->CalcColumnXBounds(static_cast(header)); + } + QRect GemRepoItemDelegate::CalcDeleteButtonRect(const QRect& contentRect) const { - const QPoint topLeft = QPoint(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2); + const int deleteHeaderEndX = CalcColumnXBounds(HeaderOrder::Delete).second; + const QPoint topLeft = QPoint(deleteHeaderEndX - s_iconSize - s_contentMargins.right(), contentRect.center().y() - s_iconSize / 2); return QRect(topLeft, QSize(s_iconSize, s_iconSize)); } QRect GemRepoItemDelegate::CalcRefreshButtonRect(const QRect& contentRect) const { - const int topLeftX = contentRect.left() + s_nameMaxWidth + s_creatorMaxWidth + s_updatedMaxWidth + s_contentSpacing * 2 + s_refreshIconSpacing; - const QPoint topLeft = QPoint(topLeftX, contentRect.center().y() - s_refreshIconSize / 3); + const int headerEndX = CalcColumnXBounds(HeaderOrder::Update).second; + const int leftX = headerEndX - s_refreshIconSize - s_refreshIconSpacing; + // Dividing size by 3 centers much better + const QPoint topLeft = QPoint(leftX, contentRect.center().y() - s_refreshIconSize / 3); return QRect(topLeft, QSize(s_refreshIconSize, s_refreshIconSize)); } void GemRepoItemDelegate::DrawEditButtons(QPainter* painter, const QRect& contentRect) const { - painter->drawPixmap(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2, m_deleteIcon); + const QRect deleteButtonRect = CalcDeleteButtonRect(contentRect); + painter->drawPixmap(deleteButtonRect, m_deleteIcon); } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h index 69d943001d..f8b53e47be 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h @@ -18,13 +18,15 @@ QT_FORWARD_DECLARE_CLASS(QEvent) namespace O3DE::ProjectManager { + QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget) + class GemRepoItemDelegate : public QStyledItemDelegate { - Q_OBJECT // AUTOMOC + Q_OBJECT public: - explicit GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent = nullptr); + explicit GemRepoItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent = nullptr); ~GemRepoItemDelegate() = default; void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override; @@ -42,15 +44,14 @@ namespace O3DE::ProjectManager inline constexpr static qreal s_fontSize = 12.0; // Margin and borders - inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/0, /*top=*/8, /*right=*/60, /*bottom=*/8); // Item border distances + inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/0, /*top=*/8, /*right=*/0, /*bottom=*/8); // Item border distances inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/20, /*right=*/20, /*bottom=*/20); // Distances of the elements within an item to the item borders inline constexpr static int s_borderWidth = 4; // Content - inline constexpr static int s_contentSpacing = 5; - inline constexpr static int s_nameMaxWidth = 145; - inline constexpr static int s_creatorMaxWidth = 115; - inline constexpr static int s_updatedMaxWidth = 125; + inline constexpr static int s_nameDefaultWidth = 150; + inline constexpr static int s_creatorDefaultWidth = 120; + inline constexpr static int s_updatedDefaultWidth = 130; // Icon inline constexpr static int s_iconSize = 24; @@ -58,6 +59,14 @@ namespace O3DE::ProjectManager inline constexpr static int s_refreshIconSize = 14; inline constexpr static int s_refreshIconSpacing = 10; + enum class HeaderOrder + { + Name, + Creator, + Update, + Delete + }; + signals: void RemoveRepo(const QModelIndex& modelIndex); void RefreshRepo(const QModelIndex& modelIndex); @@ -65,13 +74,15 @@ namespace O3DE::ProjectManager protected: void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const; QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const; - QRect CalcButtonRect(const QRect& contentRect) const; + QPair CalcColumnXBounds(HeaderOrder header) const; QRect CalcDeleteButtonRect(const QRect& contentRect) const; QRect CalcRefreshButtonRect(const QRect& contentRect) const; void DrawEditButtons(QPainter* painter, const QRect& contentRect) const; QAbstractItemModel* m_model = nullptr; + AdjustableHeaderWidget* m_headerWidget = nullptr; + QPixmap m_refreshIcon; QPixmap m_editIcon; QPixmap m_deleteIcon; diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp index 9adf3e6e3f..cf877fd73a 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp @@ -8,12 +8,15 @@ #include #include +#include #include +#include namespace O3DE::ProjectManager { - GemRepoListView::GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent) + GemRepoListView::GemRepoListView( + QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent) : QListView(parent) { setObjectName("gemRepoListView"); @@ -22,9 +25,10 @@ namespace O3DE::ProjectManager setModel(model); setSelectionModel(selectionModel); - GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, this); + GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, header, this); connect(itemDelegate, &GemRepoItemDelegate::RemoveRepo, this, &GemRepoListView::RemoveRepo); connect(itemDelegate, &GemRepoItemDelegate::RefreshRepo, this, &GemRepoListView::RefreshRepo); + connect(header, &AdjustableHeaderWidget::sectionsResized, [=] { update(); }); setItemDelegate(itemDelegate); } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h index 50bcf8daa6..7062997f09 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h @@ -17,13 +17,19 @@ QT_FORWARD_DECLARE_CLASS(QAbstractItemModel) namespace O3DE::ProjectManager { + QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget) + class GemRepoListView : public QListView { - Q_OBJECT // AUTOMOC + Q_OBJECT public: - explicit GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr); + explicit GemRepoListView( + QAbstractItemModel* model, + QItemSelectionModel* selectionModel, + AdjustableHeaderWidget* header, + QWidget* parent = nullptr); ~GemRepoListView() = default; signals: diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h index 68991a0509..d1e3975496 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h @@ -21,7 +21,7 @@ namespace O3DE::ProjectManager class GemRepoModel : public QStandardItemModel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit GemRepoModel(QObject* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp index 843538d9da..996d8873c5 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include @@ -248,6 +250,9 @@ namespace O3DE::ProjectManager QFrame* GemRepoScreen::CreateReposContent() { + constexpr int inspectorWidth = 240; + constexpr int middleLayoutIndent = 60; + QFrame* contentFrame = new QFrame(this); QHBoxLayout* hLayout = new QHBoxLayout(); @@ -255,7 +260,7 @@ namespace O3DE::ProjectManager hLayout->setSpacing(0); contentFrame->setLayout(hLayout); - hLayout->addSpacing(60); + hLayout->addSpacing(middleLayoutIndent); QVBoxLayout* middleVLayout = new QVBoxLayout(); middleVLayout->setMargin(0); @@ -287,37 +292,34 @@ namespace O3DE::ProjectManager connect(addRepoButton, &QPushButton::clicked, this, &GemRepoScreen::HandleAddRepoButton); - topMiddleHLayout->addSpacing(30); - middleVLayout->addLayout(topMiddleHLayout); middleVLayout->addSpacing(30); - // Create a QTableWidget just for its header - // Using a seperate model allows the setup of a header exactly as needed - m_gemRepoHeaderTable = new QTableWidget(this); - m_gemRepoHeaderTable->setObjectName("gemRepoHeaderTable"); - m_gemRepoListHeader = m_gemRepoHeaderTable->horizontalHeader(); - m_gemRepoListHeader->setObjectName("gemRepoListHeader"); - m_gemRepoListHeader->setDefaultAlignment(Qt::AlignLeft); - m_gemRepoListHeader->setSectionResizeMode(QHeaderView::ResizeMode::Fixed); + constexpr int minHeaderSectionWidth = 120; - // Insert columns so the header labels will show up - m_gemRepoHeaderTable->insertColumn(0); - m_gemRepoHeaderTable->insertColumn(1); - m_gemRepoHeaderTable->insertColumn(2); - m_gemRepoHeaderTable->setHorizontalHeaderLabels({ tr("Repository Name"), tr("Creator"), tr("Updated") }); + m_gemRepoHeaderTable = new AdjustableHeaderWidget( + QStringList{ tr("Repository Name"), tr("Creator"), tr("Updated"), "" }, + QVector{ + GemRepoItemDelegate::s_nameDefaultWidth, + GemRepoItemDelegate::s_creatorDefaultWidth, + GemRepoItemDelegate::s_updatedDefaultWidth + GemRepoItemDelegate::s_refreshIconSpacing + GemRepoItemDelegate::s_refreshIconSize, + // Include invisible header for delete button + GemRepoItemDelegate::s_iconSize + GemRepoItemDelegate::s_contentMargins.right() + }, + minHeaderSectionWidth, + QVector + { + QHeaderView::ResizeMode::Interactive, + QHeaderView::ResizeMode::Stretch, + QHeaderView::ResizeMode::Fixed, + QHeaderView::ResizeMode::Fixed + }, + this); - const int headerExtraMargin = 18; - m_gemRepoListHeader->resizeSection(0, GemRepoItemDelegate::s_nameMaxWidth + GemRepoItemDelegate::s_contentSpacing + headerExtraMargin); - m_gemRepoListHeader->resizeSection(1, GemRepoItemDelegate::s_creatorMaxWidth + GemRepoItemDelegate::s_contentSpacing); - m_gemRepoListHeader->resizeSection(2, GemRepoItemDelegate::s_updatedMaxWidth + GemRepoItemDelegate::s_contentSpacing); - - // Required to set stylesheet in code as it will not be respected if set in qss - m_gemRepoHeaderTable->horizontalHeader()->setStyleSheet("QHeaderView::section { background-color:transparent; color:white; font-size:12px; border-style:none; }"); middleVLayout->addWidget(m_gemRepoHeaderTable); - m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), this); + m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), m_gemRepoHeaderTable, this); middleVLayout->addWidget(m_gemRepoListView); connect(m_gemRepoListView, &GemRepoListView::RemoveRepo, this, &GemRepoScreen::HandleRemoveRepoButton); @@ -325,8 +327,10 @@ namespace O3DE::ProjectManager hLayout->addLayout(middleVLayout); + hLayout->addSpacing(middleLayoutIndent); + m_gemRepoInspector = new GemRepoInspector(m_gemRepoModel, this); - m_gemRepoInspector->setFixedWidth(240); + m_gemRepoInspector->setFixedWidth(inspectorWidth); hLayout->addWidget(m_gemRepoInspector); return contentFrame; diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h index eed9a5ec4a..643a9b91fc 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h @@ -24,6 +24,7 @@ namespace O3DE::ProjectManager QT_FORWARD_DECLARE_CLASS(GemRepoInspector) QT_FORWARD_DECLARE_CLASS(GemRepoListView) QT_FORWARD_DECLARE_CLASS(GemRepoModel) + QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget) class GemRepoScreen : public ScreenWidget @@ -59,7 +60,7 @@ namespace O3DE::ProjectManager QFrame* m_noRepoContent; QFrame* m_repoContent; - QTableWidget* m_gemRepoHeaderTable = nullptr; + AdjustableHeaderWidget* m_gemRepoHeaderTable = nullptr; QHeaderView* m_gemRepoListHeader = nullptr; GemRepoListView* m_gemRepoListView = nullptr; GemRepoInspector* m_gemRepoInspector = nullptr; diff --git a/Code/Tools/ProjectManager/Source/GemsSubWidget.h b/Code/Tools/ProjectManager/Source/GemsSubWidget.h index 5e670b930a..130e6c4282 100644 --- a/Code/Tools/ProjectManager/Source/GemsSubWidget.h +++ b/Code/Tools/ProjectManager/Source/GemsSubWidget.h @@ -22,7 +22,7 @@ namespace O3DE::ProjectManager class GemsSubWidget : public QWidget { - Q_OBJECT // AUTOMOC + Q_OBJECT public: GemsSubWidget(QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/LinkWidget.h b/Code/Tools/ProjectManager/Source/LinkWidget.h index eb0b9bb528..ab95c2ecdf 100644 --- a/Code/Tools/ProjectManager/Source/LinkWidget.h +++ b/Code/Tools/ProjectManager/Source/LinkWidget.h @@ -22,7 +22,7 @@ namespace O3DE::ProjectManager class LinkLabel : public QLabel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: LinkLabel(const QString& text = {}, const QUrl& url = {}, int fontSize = 10, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h index 358c1f249a..c526f7864d 100644 --- a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h +++ b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h @@ -31,7 +31,7 @@ namespace O3DE::ProjectManager class LabelButton : public QLabel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit LabelButton(QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h b/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h index d784fcf5fd..f184fd2e17 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h +++ b/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h @@ -11,6 +11,7 @@ namespace O3DE::ProjectManager { + inline constexpr static int MinWindowWidth = 1200; inline constexpr static int ProjectPreviewImageWidth = 210; inline constexpr static int ProjectPreviewImageHeight = 280; inline constexpr static int ProjectTemplateImageWidth = 92; diff --git a/Code/Tools/ProjectManager/Source/ScreenHeaderWidget.h b/Code/Tools/ProjectManager/Source/ScreenHeaderWidget.h index aca0d21fd3..5c000bf61d 100644 --- a/Code/Tools/ProjectManager/Source/ScreenHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/ScreenHeaderWidget.h @@ -20,7 +20,7 @@ namespace O3DE::ProjectManager class ScreenHeader : public QFrame { - Q_OBJECT // AUTOMOC + Q_OBJECT public: ScreenHeader(QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/TagWidget.h b/Code/Tools/ProjectManager/Source/TagWidget.h index fce6eaf863..df817dc506 100644 --- a/Code/Tools/ProjectManager/Source/TagWidget.h +++ b/Code/Tools/ProjectManager/Source/TagWidget.h @@ -27,7 +27,7 @@ namespace O3DE::ProjectManager class TagWidget : public QLabel { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit TagWidget(const Tag& id, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/Source/TemplateButtonWidget.h b/Code/Tools/ProjectManager/Source/TemplateButtonWidget.h index 6216f0e3de..507219d2f2 100644 --- a/Code/Tools/ProjectManager/Source/TemplateButtonWidget.h +++ b/Code/Tools/ProjectManager/Source/TemplateButtonWidget.h @@ -17,7 +17,7 @@ namespace O3DE::ProjectManager class TemplateButton : public QPushButton { - Q_OBJECT // AUTOMOC + Q_OBJECT public: explicit TemplateButton(const QString& imagePath, const QString& labelText, QWidget* parent = nullptr); diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 915b1a072f..87dfae85e8 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -81,6 +81,8 @@ set(FILES Source/TemplateButtonWidget.cpp Source/ExternalLinkDialog.h Source/ExternalLinkDialog.cpp + Source/AdjustableHeaderWidget.h + Source/AdjustableHeaderWidget.cpp Source/GemCatalog/GemCatalogHeaderWidget.h Source/GemCatalog/GemCatalogHeaderWidget.cpp Source/GemCatalog/GemCatalogScreen.h diff --git a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp index a06defff08..fa473cb8c6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include #include diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index 1aa79d1945..9462ff381f 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -11,11 +11,11 @@ #include #include #include -#include #include #include #include #include +#include #include diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp index 77031ca3af..f5aaec4faa 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp @@ -7,7 +7,7 @@ */ #include -#include +#include #include #include #include diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp index 3ff574d977..155a159447 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp @@ -6,12 +6,13 @@ * */ +#include + #include #include #include #include #include -#include #include #include #include diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.h similarity index 100% rename from Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h rename to Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.h diff --git a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake index 375dbd9724..1ec6402b3e 100644 --- a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake +++ b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake @@ -31,7 +31,7 @@ set(FILES Include/Atom/Feature/PostProcessing/PostProcessingConstants.h Include/Atom/Feature/PostProcessing/SMAAFeatureProcessorInterface.h Include/Atom/Feature/PostProcess/PostFxLayerCategoriesConstants.h - Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h + Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessorInterface.h Include/Atom/Feature/SkyBox/SkyBoxFogBus.h Include/Atom/Feature/SkyBox/SkyboxConstants.h Include/Atom/Feature/SkyBox/SkyBoxLUT.h @@ -272,7 +272,9 @@ set(FILES Source/RayTracing/RayTracingPass.cpp Source/RayTracing/RayTracingPass.h Source/RayTracing/RayTracingPassData.h + Source/ReflectionProbe/ReflectionProbeFeatureProcessor.h Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp + Source/ReflectionProbe/ReflectionProbe.h Source/ReflectionProbe/ReflectionProbe.cpp Source/ReflectionScreenSpace/ReflectionScreenSpaceTracePass.cpp Source/ReflectionScreenSpace/ReflectionScreenSpaceTracePass.h diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp index 99b3518173..ad6d81d200 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp @@ -312,8 +312,11 @@ namespace AZ const View::UsageFlags viewFlags = worklistData->m_view->GetUsageFlags(); const RHI::DrawListMask drawListMask = worklistData->m_view->GetDrawListMask(); - [[maybe_unused]] uint32_t numDrawPackets = 0; - uint32_t numVisibleCullables = 0; + #ifdef AZ_CULL_DEBUG_ENABLED + // These variable are only used for the gathering of debug information. + uint32_t numDrawPackets = 0; + uint32_t numVisibleCullables = 0; + #endif AZ_Assert(worklist.size() > 0, "Received empty worklist in ProcessWorklist"); @@ -351,8 +354,15 @@ namespace AZ if (TestOcclusionCulling(worklistData, visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) #endif { - numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); - ++numVisibleCullables; + // There are ways to write this without [[maybe_unused]], but they are brittle. + // For example, using #else could cause a bug where the function's parameter + // is changed in #ifdef but not in #else. + [[maybe_unused]] const uint32_t drawPacketCount=AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); + #ifdef AZ_CULL_DEBUG_ENABLED + ++numVisibleCullables; + numDrawPackets += drawPacketCount; + #endif + c->m_isVisible = true; } } @@ -387,8 +397,15 @@ namespace AZ if (TestOcclusionCulling(worklistData, visibleEntry) == MaskedOcclusionCulling::CullingResult::VISIBLE) #endif { - numDrawPackets += AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); - ++numVisibleCullables; + // There are ways to write this without [[maybe_unused]], but they are brittle. + // For example, using #else could cause a bug where the function's parameter + // is changed in #ifdef but not in #else. + [[maybe_unused]] const uint32_t drawPacketCount=AddLodDataToView(c->m_cullData.m_boundingSphere.GetCenter(), c->m_lodData, *worklistData->m_view); + #ifdef AZ_CULL_DEBUG_ENABLED + ++numVisibleCullables; + numDrawPackets += drawPacketCount; + #endif + c->m_isVisible = true; } } diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp index 660f8e52d7..c39012d303 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp @@ -28,8 +28,6 @@ namespace EMStudio { AZ_CLASS_ALLOCATOR_IMPL(AtomRenderPlugin, EMotionFX::EditorAllocator, 0); - const AzToolsFramework::ManipulatorManagerId g_animManipulatorManagerId = - AzToolsFramework::ManipulatorManagerId(AZ::Crc32("AnimManipulatorManagerId")); AtomRenderPlugin::AtomRenderPlugin() : DockWidgetPlugin() diff --git a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.cpp b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.cpp index 6881e49429..efc895c0b5 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.cpp @@ -500,6 +500,9 @@ namespace EMStudio painter.drawPath(path); } + const AzToolsFramework::ManipulatorManagerId g_animManipulatorManagerId = + AzToolsFramework::ManipulatorManagerId(AZ::Crc32("AnimManipulatorManagerId")); + // shortcuts QApplication* GetApp() { diff --git a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.h b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.h index 1d60773e1f..753086ca01 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.h +++ b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/EMStudioManager.h @@ -31,6 +31,8 @@ #include "MainWindow.h" #include +#include + // include Qt #include #include @@ -169,6 +171,9 @@ namespace EMStudio EventProcessingCallback* m_eventProcessingCallback = nullptr; }; + // Define the manipulator id for atom viewport in animation editor. + extern const AzToolsFramework::ManipulatorManagerId g_animManipulatorManagerId; + // Shortcuts QApplication* GetApp(); EMStudioManager* GetManager(); diff --git a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.cpp b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.cpp index 7a24ece95a..9993161b70 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.cpp @@ -25,6 +25,8 @@ namespace EMStudio , m_currentValue(0.0f, 0.0f, 0.0f) , m_gizmoButton(nullptr) , m_transformationGizmo(nullptr) + , m_translationManipulators( + AzToolsFramework::TranslationManipulators::Dimensions::Three, AZ::Transform::Identity(), AZ::Vector3::CreateOne()) { UpdateValue(); } @@ -102,6 +104,27 @@ namespace EMStudio m_gizmoButton->setCheckable(true); m_gizmoButton->setEnabled(!IsReadOnly()); m_manipulatorCallback = manipulatorCallback; + + // Setup the translation manipulator + AzToolsFramework::ConfigureTranslationManipulatorAppearance3d(&m_translationManipulators); + m_translationManipulators.InstallLinearManipulatorMouseMoveCallback( + [this](const AzToolsFramework::LinearManipulator::Action& action) + { + OnManipulatorMoved(action.LocalPosition()); + }); + + m_translationManipulators.InstallPlanarManipulatorMouseMoveCallback( + [this](const AzToolsFramework::PlanarManipulator::Action& action) + { + OnManipulatorMoved(action.LocalPosition()); + }); + + m_translationManipulators.InstallSurfaceManipulatorMouseMoveCallback( + [this](const AzToolsFramework::SurfaceManipulator::Action& action) + { + OnManipulatorMoved(action.LocalPosition()); + }); + return m_gizmoButton; } @@ -183,6 +206,17 @@ namespace EMStudio EMStudioManager::MakeTransparentButton(m_gizmoButton, "Images/Icons/Vector3GizmoDisabled.png", "Show/Hide translation gizmo for visual manipulation"); } + // These will enable/disable the translation manipulator for atom render viewport. + if (m_translationManipulators.Registered()) + { + m_translationManipulators.Unregister(); + } + else + { + m_translationManipulators.Register(g_animManipulatorManagerId); + } + + // These will enable/disable the translation manipulator for opengl render viewport. if (!m_transformationGizmo) { m_transformationGizmo = static_cast(GetManager()->AddTransformationManipulator(new MCommon::TranslateManipulator(70.0f, true))); @@ -197,4 +231,14 @@ namespace EMStudio m_transformationGizmo = nullptr; } } + + void Vector3GizmoParameterEditor::OnManipulatorMoved(const AZ::Vector3& position) + { + m_translationManipulators.SetLocalPosition(position); + SetValue(position); + if (m_manipulatorCallback) + { + m_manipulatorCallback(); + } + } } diff --git a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.h b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.h index 92dd40923d..71d6d63cda 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.h +++ b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/Plugins/StandardPlugins/Source/AnimGraph/ParameterEditor/Vector3GizmoParameterEditor.h @@ -8,7 +8,8 @@ #pragma once -#include "ValueParameterEditor.h" +#include +#include #include @@ -50,10 +51,16 @@ namespace EMStudio AZ::Vector3 GetMinValue() const; AZ::Vector3 GetMaxValue() const; + void OnManipulatorMoved(const AZ::Vector3& position); + private: AZ::Vector3 m_currentValue = AZ::Vector3::CreateZero(); QPushButton* m_gizmoButton = nullptr; + + // TODO: Remove this when we remove the opengl widget MCommon::TranslateManipulator* m_transformationGizmo = nullptr; + + AzToolsFramework::TranslationManipulators m_translationManipulators; AZStd::function m_manipulatorCallback; }; } // namespace EMStudio diff --git a/Gems/MotionMatching/Assets/Animations/Acceleration1.fbx b/Gems/MotionMatching/Assets/Animations/Acceleration1.fbx new file mode 100644 index 0000000000..c2c167cdd1 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Acceleration1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33d5b3035966ac54bbb97a207ca14868a6daef9ad9e596281f7930764b54c819 +size 2527664 diff --git a/Gems/MotionMatching/Assets/Animations/Circles1.fbx b/Gems/MotionMatching/Assets/Animations/Circles1.fbx new file mode 100644 index 0000000000..fcdecc0ab5 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Circles1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91d6a8c16bae554339d1849285804b798e136113c6aecc3dfed1be635c484600 +size 4750720 diff --git a/Gems/MotionMatching/Assets/Animations/Crouching1.fbx b/Gems/MotionMatching/Assets/Animations/Crouching1.fbx new file mode 100644 index 0000000000..bf5c171f8b --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Crouching1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e18101baa7b33af4ac26181b7c3c20d5c12e9d9d2f57bd24271c6e4665e1a65a +size 3001296 diff --git a/Gems/MotionMatching/Assets/Animations/FreeRoaming1.fbx b/Gems/MotionMatching/Assets/Animations/FreeRoaming1.fbx new file mode 100644 index 0000000000..f44eba537a --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/FreeRoaming1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eda027438797e48e6b00745e17e3ec4da6d507735c754056a8fea19c465a528 +size 5926096 diff --git a/Gems/MotionMatching/Assets/Animations/FreeRoaming2.fbx b/Gems/MotionMatching/Assets/Animations/FreeRoaming2.fbx new file mode 100644 index 0000000000..837f8df648 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/FreeRoaming2.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2af3ea28464b4d7269f54c1ff1ded691b19b826d5de85139d20ae825aae992 +size 5085168 diff --git a/Gems/MotionMatching/Assets/Animations/Jog1.fbx b/Gems/MotionMatching/Assets/Animations/Jog1.fbx new file mode 100644 index 0000000000..02e6c73ac5 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Jog1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25593a1e4670dbb9cb1c5a0fc375ba2bf7862784c45847d76eefd44d39df86f9 +size 2974896 diff --git a/Gems/MotionMatching/Assets/Animations/JogPivotTurn1.fbx b/Gems/MotionMatching/Assets/Animations/JogPivotTurn1.fbx new file mode 100644 index 0000000000..51fac75ca6 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/JogPivotTurn1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e21e3f9b7db6572f740a99159bba72eca3697d3d3e1c07562579be9599bcebb7 +size 2511488 diff --git a/Gems/MotionMatching/Assets/Animations/Jumps1.fbx b/Gems/MotionMatching/Assets/Animations/Jumps1.fbx new file mode 100644 index 0000000000..d075da8991 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Jumps1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3802b0e44b3e0aa6610a4157510c3038bfe4038211202cebd6eb116ffd53cee9 +size 2587408 diff --git a/Gems/MotionMatching/Assets/Animations/JumpsFreeRoam1.fbx b/Gems/MotionMatching/Assets/Animations/JumpsFreeRoam1.fbx new file mode 100644 index 0000000000..d384c9f156 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/JumpsFreeRoam1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f07e278fcee81d719ec2e7c02417e2ee710b9d224a29798cfb97b55a9ffe351 +size 3515488 diff --git a/Gems/MotionMatching/Assets/Animations/MixedLocomotion1.fbx b/Gems/MotionMatching/Assets/Animations/MixedLocomotion1.fbx new file mode 100644 index 0000000000..317fa22e76 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/MixedLocomotion1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc4d0b653f910cea3c7c3b8d08963af0cc331ad91e467c7becb2c7f9b9c5b402 +size 4068144 diff --git a/Gems/MotionMatching/Assets/Animations/OutofRange1.fbx b/Gems/MotionMatching/Assets/Animations/OutofRange1.fbx new file mode 100644 index 0000000000..ecde12dee1 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/OutofRange1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58cd43568cdd3f9b9585b65208cfff6d6c5f5bea9ad67878a1efbeebb8ef8a6d +size 1812176 diff --git a/Gems/MotionMatching/Assets/Animations/Pushes1.fbx b/Gems/MotionMatching/Assets/Animations/Pushes1.fbx new file mode 100644 index 0000000000..d89c45a611 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Pushes1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18a831cfe702120d163e44a27d5cd33faf64f5f892da3789aea1bbfecd528e72 +size 3288400 diff --git a/Gems/MotionMatching/Assets/Animations/Run1.fbx b/Gems/MotionMatching/Assets/Animations/Run1.fbx new file mode 100644 index 0000000000..150ae00f4f --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Run1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3649e2d0b239f9da45af2b04e937ce6113e8dee74a70a3cef748995ea15f6120 +size 2354544 diff --git a/Gems/MotionMatching/Assets/Animations/RunPivotTurn1.fbx b/Gems/MotionMatching/Assets/Animations/RunPivotTurn1.fbx new file mode 100644 index 0000000000..8e67a72016 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/RunPivotTurn1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:207378e42ebdad1c3dfe8cefc84ed50e1d322895f018e2efca14fdcbf2e6600f +size 1890352 diff --git a/Gems/MotionMatching/Assets/Animations/Snake1.fbx b/Gems/MotionMatching/Assets/Animations/Snake1.fbx new file mode 100644 index 0000000000..ff597b968d --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Snake1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78c68566aba845544ec96a92f2cf4e36350c74c16e931c3aa9e14151bcaed8e8 +size 3881680 diff --git a/Gems/MotionMatching/Assets/Animations/TurnOnSpot1.fbx b/Gems/MotionMatching/Assets/Animations/TurnOnSpot1.fbx new file mode 100644 index 0000000000..ac71a0f42b --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/TurnOnSpot1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73560b662d3819647985c3f7b0544ba3ad71365e8393a3915630b254f86c6286 +size 3669552 diff --git a/Gems/MotionMatching/Assets/Animations/Walk1.fbx b/Gems/MotionMatching/Assets/Animations/Walk1.fbx new file mode 100644 index 0000000000..423a59b31f --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Walk1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2fad264af33252d49c6e3a0b6c6ef83697b951a54c7ab0cd4259f145024d915 +size 3962336 diff --git a/Gems/MotionMatching/Assets/Animations/Walk2.fbx b/Gems/MotionMatching/Assets/Animations/Walk2.fbx new file mode 100644 index 0000000000..0aaf74b496 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/Walk2.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:149ae8bfc0dc4ad8471e79f5cd8ce5420c02bc5c8a5cf1971ebbf997cae21096 +size 3834704 diff --git a/Gems/MotionMatching/Assets/Animations/WalkPivotTurn1.fbx b/Gems/MotionMatching/Assets/Animations/WalkPivotTurn1.fbx new file mode 100644 index 0000000000..1dac028839 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/WalkPivotTurn1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6468a660cfd88c350443e92b31e41362909d705c344cda6640c65a5e1fd2a29 +size 2288048 diff --git a/Gems/MotionMatching/Assets/Animations/WalkStopTurn1.fbx b/Gems/MotionMatching/Assets/Animations/WalkStopTurn1.fbx new file mode 100644 index 0000000000..20f967fece --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/WalkStopTurn1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b824d6fd5bb3e7f6b4f29ff90b406179539e1c155ba9f8870ef54f1d1d0e22e7 +size 3128032 diff --git a/Gems/MotionMatching/Assets/Animations/WalkStopTurnPivot1.fbx b/Gems/MotionMatching/Assets/Animations/WalkStopTurnPivot1.fbx new file mode 100644 index 0000000000..50f69af25c --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/WalkStopTurnPivot1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f0f9e280ff686032aa223cc8ce754442edc616d28a16d39a87c90bd221c91a +size 2807280 diff --git a/Gems/MotionMatching/Assets/Animations/WalkTurns1.fbx b/Gems/MotionMatching/Assets/Animations/WalkTurns1.fbx new file mode 100644 index 0000000000..acdb27e1d9 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/WalkTurns1.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862ddf8893923169938db017f573f63228bb5aec3d4892d21a525200dd6c4680 +size 4063280 diff --git a/Gems/MotionMatching/Assets/Animations/acceleration1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/acceleration1.fbx.assetinfo new file mode 100644 index 0000000000..c9749b8bf3 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/acceleration1.fbx.assetinfo @@ -0,0 +1,50 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "acceleration1", + "selectedRootBone": "RootNode.root", + "id": "{ADB2CDC1-8EA3-5B21-90D6-43EBE9991709}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 19.600000381469727, + "endTime": 20.666667938232422 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 50.599998474121094, + "endTime": 53.19999694824219 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/circles1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/circles1.fbx.assetinfo new file mode 100644 index 0000000000..4276767f05 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/circles1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "circles1", + "selectedRootBone": "RootNode.root", + "id": "{BF21E0D5-87F6-5A3F-B100-507F217D4C7E}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 112.73334503173828, + "endTime": 114.00001525878906 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/crouching1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/crouching1.fbx.assetinfo new file mode 100644 index 0000000000..2f2f8c52e5 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/crouching1.fbx.assetinfo @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/MotionMatching/Assets/Animations/freeroaming1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/freeroaming1.fbx.assetinfo new file mode 100644 index 0000000000..0a6534208d --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/freeroaming1.fbx.assetinfo @@ -0,0 +1,59 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "freeroaming1", + "selectedRootBone": "RootNode.root", + "id": "{A07E54E7-BB49-5DB3-BCA1-5EC8B4FA74A3}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 73.33333587646484, + "endTime": 111.13333129882813 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 134.3333282470703, + "endTime": 136.13333129882813 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 145.1999969482422, + "endTime": 146.13333129882813 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/freeroaming2.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/freeroaming2.fbx.assetinfo new file mode 100644 index 0000000000..bdb6c10668 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/freeroaming2.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "freeroaming2", + "selectedRootBone": "RootNode.root", + "id": "{96DC1ABD-1F72-5546-8B7F-7092C3AC0E5D}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 119.26667022705078, + "endTime": 123.4000015258789 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/jog1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/jog1.fbx.assetinfo new file mode 100644 index 0000000000..dbb83cb4d3 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/jog1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "jog1", + "selectedRootBone": "RootNode.root", + "id": "{9200D325-808C-5B2D-B323-1FF9790C07B7}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 64.0, + "endTime": 65.53333282470703 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/jogpivotturn1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/jogpivotturn1.fbx.assetinfo new file mode 100644 index 0000000000..10b3e2e59a --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/jogpivotturn1.fbx.assetinfo @@ -0,0 +1,53 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "jogpivotturn1", + "selectedRootBone": "RootNode.root", + "id": "{596210E7-A7F4-511D-886B-AA4FED4AC92B}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false + }, + { + "name": "Event Track 2", + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 36.66666793823242, + "endTime": 40.733333587646484 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 41.733333587646484, + "endTime": 53.06666564941406 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/mixedlocomotion1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/mixedlocomotion1.fbx.assetinfo new file mode 100644 index 0000000000..d8d1a4d4e6 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/mixedlocomotion1.fbx.assetinfo @@ -0,0 +1,53 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "mixedlocomotion1", + "selectedRootBone": "RootNode.root", + "id": "{2F7682E4-235E-5A31-B450-266D7DC00E39}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false + }, + { + "name": "Event Track 2", + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 27.53333282470703, + "endTime": 29.999998092651367 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 56.53333282470703, + "endTime": 60.666664123535156 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/outofrange1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/outofrange1.fbx.assetinfo new file mode 100644 index 0000000000..9e206c09ae --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/outofrange1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "outofrange1", + "selectedRootBone": "RootNode.root", + "id": "{6B28C886-471C-5506-AD03-DF19025F6DA1}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 29.299814224243164, + "endTime": 33.83555221557617 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/run1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/run1.fbx.assetinfo new file mode 100644 index 0000000000..fe0bd2f197 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/run1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "run1", + "selectedRootBone": "RootNode.root", + "id": "{12953346-AF3A-5481-A54F-9119523C4538}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 23.600000381469727, + "endTime": 27.933334350585938 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/runpivotturn1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/runpivotturn1.fbx.assetinfo new file mode 100644 index 0000000000..353f4bd81c --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/runpivotturn1.fbx.assetinfo @@ -0,0 +1,50 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "runpivotturn1", + "selectedRootBone": "RootNode.root", + "id": "{DEF0D469-00AB-57D0-AF05-6AF1D6563D4A}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 25.080034255981445, + "endTime": 28.964109420776367 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 29.574464797973633, + "endTime": 36.288368225097656 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/snake1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/snake1.fbx.assetinfo new file mode 100644 index 0000000000..5c8961c700 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/snake1.fbx.assetinfo @@ -0,0 +1,50 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "snake1", + "selectedRootBone": "RootNode.root", + "id": "{4A29F10E-0083-559F-A78B-282A9EF87E00}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 34.46666717529297, + "endTime": 38.733333587646484 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 89.4000015258789, + "endTime": 90.86666870117188 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/turnonspot1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/turnonspot1.fbx.assetinfo new file mode 100644 index 0000000000..76c0729bb2 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/turnonspot1.fbx.assetinfo @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/MotionMatching/Assets/Animations/walk1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walk1.fbx.assetinfo new file mode 100644 index 0000000000..f21f187899 --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walk1.fbx.assetinfo @@ -0,0 +1,59 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "walk1", + "selectedRootBone": "RootNode.root", + "id": "{B161FB42-0EC0-51DA-BB0E-F04B73C0DE0C}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 11.533333778381348, + "endTime": 12.533333778381348 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 35.20000076293945, + "endTime": 37.53333282470703 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 76.33333587646484, + "endTime": 93.66667175292969 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/walk2.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walk2.fbx.assetinfo new file mode 100644 index 0000000000..c945ba6a6d --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walk2.fbx.assetinfo @@ -0,0 +1,50 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "walk2", + "selectedRootBone": "RootNode.root", + "id": "{D4D1809B-5085-59E4-B98C-D29AE1A90277}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 37.599998474121094, + "endTime": 40.266666412353516 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 81.26667022705078, + "endTime": 84.0666732788086 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/walkpivotturn1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walkpivotturn1.fbx.assetinfo new file mode 100644 index 0000000000..f603a5858e --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walkpivotturn1.fbx.assetinfo @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/MotionMatching/Assets/Animations/walkstopturn1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walkstopturn1.fbx.assetinfo new file mode 100644 index 0000000000..ffd5dbbd0e --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walkstopturn1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "walkstopturn1", + "selectedRootBone": "RootNode.root", + "id": "{D38F0C22-1841-5EBB-A198-9D9441CD7C80}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 63.53333282470703, + "endTime": 70.53333282470703 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/walkstopturnpivot1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walkstopturnpivot1.fbx.assetinfo new file mode 100644 index 0000000000..121124029b --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walkstopturnpivot1.fbx.assetinfo @@ -0,0 +1,41 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "walkstopturnpivot1", + "selectedRootBone": "RootNode.root", + "id": "{FCD5DF16-A875-552E-9333-3C7BF8554CBB}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 53.06666564941406, + "endTime": 55.86666488647461 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Animations/walkturns1.fbx.assetinfo b/Gems/MotionMatching/Assets/Animations/walkturns1.fbx.assetinfo new file mode 100644 index 0000000000..5f200f546e --- /dev/null +++ b/Gems/MotionMatching/Assets/Animations/walkturns1.fbx.assetinfo @@ -0,0 +1,50 @@ +{ + "values": [ + { + "$type": "MotionGroup", + "name": "walkturns1", + "selectedRootBone": "RootNode.root", + "id": "{FD1981AB-0270-56F7-9062-ABA4D73686F9}", + "rules": { + "rules": [ + { + "$type": "EMotionFX::Pipeline::Rule::MotionMetaDataRule", + "data": { + "motionEventTable": { + "tracks": [ + { + "name": "Sync", + "deletable": false, + "events": [ + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 32.266666412353516, + "endTime": 51.33333206176758 + }, + { + "eventDatas": [ + { + "$type": "DiscardFrameEventData" + } + ], + "startTime": 86.86666870117188, + "endTime": 89.46666717529297 + } + ] + } + ] + } + } + }, + { + "$type": "MotionSamplingRule" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/Character/Rin.fbx b/Gems/MotionMatching/Assets/Character/Rin.fbx new file mode 100644 index 0000000000..3041a9efe1 --- /dev/null +++ b/Gems/MotionMatching/Assets/Character/Rin.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d38ee57dcf86d1209982ffa29cf5a2b42f5e0e0b86f5045a8485e3e431d74b03 +size 12267120 diff --git a/Gems/MotionMatching/Assets/Character/Rin.fbx.assetinfo b/Gems/MotionMatching/Assets/Character/Rin.fbx.assetinfo new file mode 100644 index 0000000000..1a3d8be596 --- /dev/null +++ b/Gems/MotionMatching/Assets/Character/Rin.fbx.assetinfo @@ -0,0 +1,1543 @@ +{ + "values": [ + { + "$type": "ActorGroup", + "name": "RinMM", + "id": "{3B0C7D44-39A9-5B89-B361-4789175AE832}", + "rules": { + "rules": [ + { + "$type": "MetaDataRule", + "metaData": "AdjustActor -actorID $(ACTORID) -name \"RinMM\"\nActorSetCollisionMeshes -actorID $(ACTORID) -lod 0 -nodeList \"\"\nAdjustActor -actorID $(ACTORID) -nodesExcludedFromBounds \"\" -nodeAction \"select\"\nAdjustActor -actorID $(ACTORID) -nodeAction \"replace\" -attachmentNodes \"\"\nAdjustActor -actorID $(ACTORID) -motionExtractionNodeName \"root\"\nAdjustActor -actorID $(ACTORID) -mirrorSetup \"L_leg_JNT,R_leg_JNT;R_leg_JNT,L_leg_JNT;L_ribbon_01_JNT,R_ribbon_01_JNT;R_ribbon_01_JNT,L_ribbon_01_JNT;L_knee_JNT,R_knee_JNT;L_leg_twist_JNT,R_leg_twist_JNT;R_knee_JNT,L_knee_JNT;R_leg_twist_JNT,L_leg_twist_JNT;L_ribbon_02_JNT,R_ribbon_02_JNT;R_ribbon_02_JNT,L_ribbon_02_JNT;L_foot_JNT,R_foot_JNT;L_knee_twist_JNT,R_knee_twist_JNT;R_foot_JNT,L_foot_JNT;R_knee_twist_JNT,L_knee_twist_JNT;L_toe_JNT,R_toe_JNT;R_toe_JNT,L_toe_JNT;L_clavicle_JNT,R_clavicle_JNT;R_clavicle_JNT,L_clavicle_JNT;L_neckCollar_01_JNT,R_neckCollar_01_JNT;L_neckCollar_02_JNT,R_neckCollar_02_JNT;R_neckCollar_01_JNT,L_neckCollar_01_JNT;R_neckCollar_02_JNT,L_neckCollar_02_JNT;L_arm_JNT,R_arm_JNT;L_tassle_01_JNT,R_tassle_01_JNT;L_tassleLoop_01_JNT,R_tassleLoop_01_JNT;L_armPit_corr_JNT,R_armPit_corr_JNT;R_arm_JNT,L_arm_JNT;R_tassle_01_JNT,L_tassle_01_JNT;R_tassleLoop_01_JNT,L_tassleLoop_01_JNT;R_armPit_corr_JNT,L_armPit_corr_JNT;L_elbow_JNT,R_elbow_JNT;L_arm_twist_JNT,R_arm_twist_JNT;L_armBulge_corr_JNT,R_armBulge_corr_JNT;L_tassle_02_JNT,R_tassle_02_JNT;L_tassleLoop_02_JNT,R_tassleLoop_02_JNT;R_elbow_JNT,L_elbow_JNT;R_arm_twist_JNT,L_arm_twist_JNT;R_armBulge_corr_JNT,L_armBulge_corr_JNT;R_tassle_02_JNT,L_tassle_02_JNT;R_tassleLoop_02_JNT,L_tassleLoop_02_JNT;L_wrist_JNT,R_wrist_JNT;L_elbow_twist_JNT,R_elbow_twist_JNT;L_tassle_03_JNT,R_tassle_03_JNT;L_brow_inner_JNT,R_brow_inner_JNT;L_brow_mid_JNT,R_brow_mid_JNT;L_brow_outer_JNT,R_brow_outer_JNT;L_nostril_inner_JNT,R_nostril_inner_JNT;L_nostril_outer_JNT,R_nostril_outer_JNT;L_cheekUpper_inner_JNT,R_cheekUpper_inner_JNT;L_squint_mid_JNT,R_squint_mid_JNT;L_squint_outer_JNT,R_squint_outer_JNT;L_squint_inner_JNT,R_squint_inner_JNT;L_zygomatic_outer_JNT,R_zygomatic_outer_JNT;L_cheekBone_JNT,R_cheekBone_JNT;L_ear_JNT,R_ear_JNT;L_chin_below_JNT,R_chin_below_JNT;L_eyelid_lower_JNT,R_eyelid_lower_JNT;L_eyelid_upper_JNT,R_eyelid_upper_JNT;L_eye_JNT,R_eye_JNT;L_lipUpper_JNT,R_lipUpper_JNT;L_lipLevator_inner_JNT,R_lipLevator_inner_JNT;L_lipLevator_corner_JNT,R_lipLevator_corner_JNT;L_cheekUpper_outer_JNT,R_cheekUpper_outer_JNT;L_cheekUpper_mid_JNT,R_cheekUpper_mid_JNT;R_eyelid_upper_JNT,L_eyelid_upper_JNT;R_eyelid_lower_JNT,L_eyelid_lower_JNT;R_nostril_inner_JNT,L_nostril_inner_JNT;R_lipLevator_inner_JNT,L_lipLevator_inner_JNT;R_cheekBone_JNT,L_cheekBone_JNT;R_zygomatic_outer_JNT,L_zygomatic_outer_JNT;R_lipUpper_JNT,L_lipUpper_JNT;R_chin_below_JNT,L_chin_below_JNT;R_ear_JNT,L_ear_JNT;R_eye_JNT,L_eye_JNT;R_brow_inner_JNT,L_brow_inner_JNT;R_brow_mid_JNT,L_brow_mid_JNT;R_brow_outer_JNT,L_brow_outer_JNT;R_lipLevator_corner_JNT,L_lipLevator_corner_JNT;R_cheekUpper_outer_JNT,L_cheekUpper_outer_JNT;R_cheekUpper_mid_JNT,L_cheekUpper_mid_JNT;R_nostril_outer_JNT,L_nostril_outer_JNT;R_cheekUpper_inner_JNT,L_cheekUpper_inner_JNT;R_squint_inner_JNT,L_squint_inner_JNT;R_squint_mid_JNT,L_squint_mid_JNT;R_squint_outer_JNT,L_squint_outer_JNT;L_lipUpper_corner_JNT,R_lipUpper_corner_JNT;R_lipUpper_corner_JNT,L_lipUpper_corner_JNT;R_frontalis_inner_JNT,L_frontalis_inner_JNT;R_frontalis_outer_JNT,L_frontalis_outer_JNT;L_frontalis_outer_JNT,R_frontalis_outer_JNT;L_frontalis_inner_JNT,R_frontalis_inner_JNT;R_eyelid_fold_JNT,L_eyelid_fold_JNT;L_eyelid_fold_JNT,R_eyelid_fold_JNT;L_eye_bulge_JNT,R_eye_bulge_JNT;R_eye_bulge_JNT,L_eye_bulge_JNT;R_wrist_JNT,L_wrist_JNT;R_elbow_twist_JNT,L_elbow_twist_JNT;R_tassle_03_JNT,L_tassle_03_JNT;L_thumb_01_JNT,R_thumb_01_JNT;L_index_root_JNT,R_index_root_JNT;L_middle_root_JNT,R_middle_root_JNT;L_ring_root_JNT,R_ring_root_JNT;L_pinky_root_JNT,R_pinky_root_JNT;L_depressor_JNT,R_depressor_JNT;R_depressor_JNT,L_depressor_JNT;L_lip_nasolabial_JNT,R_lip_nasolabial_JNT;R_mouth_corner_JNT,L_mouth_corner_JNT;R_lip_nasolabial_JNT,L_lip_nasolabial_JNT;R_lipLower_corner_JNT,L_lipLower_corner_JNT;R_lipLower_JNT,L_lipLower_JNT;L_lipLower_JNT,R_lipLower_JNT;L_mouth_corner_JNT,R_mouth_corner_JNT;L_lipLower_corner_JNT,R_lipLower_corner_JNT;R_jaw_clench_JNT,L_jaw_clench_JNT;L_jaw_clench_JNT,R_jaw_clench_JNT;L_zygomatic_inner_JNT,R_zygomatic_inner_JNT;R_zygomatic_inner_JNT,L_zygomatic_inner_JNT;R_thumb_01_JNT,L_thumb_01_JNT;R_index_root_JNT,L_index_root_JNT;R_middle_root_JNT,L_middle_root_JNT;R_ring_root_JNT,L_ring_root_JNT;R_pinky_root_JNT,L_pinky_root_JNT;L_thumb_02_JNT,R_thumb_02_JNT;L_index_01_JNT,R_index_01_JNT;L_middle_01_JNT,R_middle_01_JNT;L_ring_01_JNT,R_ring_01_JNT;L_pinky_01_JNT,R_pinky_01_JNT;R_thumb_02_JNT,L_thumb_02_JNT;R_index_01_JNT,L_index_01_JNT;R_middle_01_JNT,L_middle_01_JNT;R_ring_01_JNT,L_ring_01_JNT;R_pinky_01_JNT,L_pinky_01_JNT;L_thumb_03_JNT,R_thumb_03_JNT;L_index_02_JNT,R_index_02_JNT;L_middle_02_JNT,R_middle_02_JNT;L_ring_02_JNT,R_ring_02_JNT;L_pinky_02_JNT,R_pinky_02_JNT;R_thumb_03_JNT,L_thumb_03_JNT;R_index_02_JNT,L_index_02_JNT;R_middle_02_JNT,L_middle_02_JNT;R_ring_02_JNT,L_ring_02_JNT;R_pinky_02_JNT,L_pinky_02_JNT;L_index_03_JNT,R_index_03_JNT;L_middle_03_JNT,R_middle_03_JNT;L_ring_03_JNT,R_ring_03_JNT;L_pinky_03_JNT,R_pinky_03_JNT;R_index_03_JNT,L_index_03_JNT;R_middle_03_JNT,L_middle_03_JNT;R_ring_03_JNT,L_ring_03_JNT;R_pinky_03_JNT,L_pinky_03_JNT;\"\n" + } + ] + } + }, + { + "$type": "{5B03C8E6-8CEE-4DA0-A7FA-CD88689DD45B} MeshGroup", + "id": "{3C9D4C02-8F36-5F94-8B47-CEC412E736F3}", + "name": "anigmarinactor", + "NodeSelectionList": { + "unselectedNodes": [ + "RootNode", + "RootNode.rin_eyeballs", + "RootNode.rin_haircap", + "RootNode.rin_cloth", + "RootNode.rin_leather", + "RootNode.rin_armor", + "RootNode.rin_hands", + "RootNode.rin_props", + "RootNode.rin_teeth_low", + "RootNode.rin_teeth_up", + "RootNode.rin_tongue", + "RootNode.rin_face", + "RootNode.rin_armorstraps", + "RootNode.rin_hairplanes", + "RootNode.rin_eyebrows_top", + "RootNode.rin_eyelashes_top", + "RootNode.rin_eyelashes_lower", + "RootNode.rin_eyecover", + "RootNode.rin_facefuzz", + "RootNode.rin_haircards", + "RootNode.rin_lash_01", + "RootNode.rin_lash_02", + "RootNode.rin_lash_03", + "RootNode.rin_eyewetness", + "RootNode.rin_eyebrows_lower", + "RootNode.rin_tearduct", + "RootNode.root", + "RootNode.rin_eyeballs.rin_eyeballs_1", + "RootNode.rin_eyeballs.rin_eyeballs_2", + "RootNode.rin_haircap.rin_haircap_1", + "RootNode.rin_haircap.rin_haircap_2", + "RootNode.rin_cloth.rin_cloth_1", + "RootNode.rin_cloth.rin_cloth_2", + "RootNode.rin_leather.rin_leather_1", + "RootNode.rin_leather.rin_leather_2", + "RootNode.rin_armor.rin_armor_1", + "RootNode.rin_armor.rin_armor_2", + "RootNode.rin_hands.rin_hands_1", + "RootNode.rin_hands.rin_hands_2", + "RootNode.rin_props.rin_props_1", + "RootNode.rin_props.rin_props_2", + "RootNode.rin_teeth_low.rin_teeth_low_1", + "RootNode.rin_teeth_low.rin_teeth_low_2", + "RootNode.rin_teeth_up.rin_teeth_up_1", + "RootNode.rin_teeth_up.rin_teeth_up_2", + "RootNode.rin_tongue.rin_tongue_1", + "RootNode.rin_tongue.rin_tongue_2", + "RootNode.rin_face.rin_face_1", + "RootNode.rin_face.rin_face_2", + "RootNode.rin_armorstraps.rin_armorstraps_1", + "RootNode.rin_armorstraps.rin_armorstraps_2", + "RootNode.rin_hairplanes.rin_hairplanes_1", + "RootNode.rin_hairplanes.rin_hairplanes_2", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2", + "RootNode.rin_eyecover.rin_eyecover_1", + "RootNode.rin_eyecover.rin_eyecover_2", + "RootNode.rin_facefuzz.rin_facefuzz_1", + "RootNode.rin_facefuzz.rin_facefuzz_2", + "RootNode.rin_haircards.rin_haircards_1", + "RootNode.rin_haircards.rin_haircards_2", + "RootNode.rin_lash_01.rin_lash_01_1", + "RootNode.rin_lash_01.rin_lash_01_2", + "RootNode.rin_lash_02.rin_lash_02_1", + "RootNode.rin_lash_02.rin_lash_02_2", + "RootNode.rin_lash_03.rin_lash_03_1", + "RootNode.rin_lash_03.rin_lash_03_2", + "RootNode.rin_eyewetness.rin_eyewetness_1", + "RootNode.rin_eyewetness.rin_eyewetness_2", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2", + "RootNode.rin_tearduct.rin_tearduct_1", + "RootNode.rin_tearduct.rin_tearduct_2", + "RootNode.root.C_pelvis_JNT", + "RootNode.rin_eyeballs.rin_eyeballs_1.Bitangent", + "RootNode.rin_eyeballs.rin_eyeballs_1.SkinWeight_", + "RootNode.rin_eyeballs.rin_eyeballs_1.transform", + "RootNode.rin_eyeballs.rin_eyeballs_1.Tangent", + "RootNode.rin_eyeballs.rin_eyeballs_1.map1", + "RootNode.rin_eyeballs.rin_eyeballs_1.rin_m_eyeballs", + "RootNode.rin_eyeballs.rin_eyeballs_2.Bitangent", + "RootNode.rin_eyeballs.rin_eyeballs_2.SkinWeight_", + "RootNode.rin_eyeballs.rin_eyeballs_2.transform", + "RootNode.rin_eyeballs.rin_eyeballs_2.Tangent", + "RootNode.rin_eyeballs.rin_eyeballs_2.map1", + "RootNode.rin_eyeballs.rin_eyeballs_2.rin_m_eyeballs", + "RootNode.rin_haircap.rin_haircap_1.Bitangent", + "RootNode.rin_haircap.rin_haircap_1.SkinWeight_", + "RootNode.rin_haircap.rin_haircap_1.transform", + "RootNode.rin_haircap.rin_haircap_1.Tangent", + "RootNode.rin_haircap.rin_haircap_1.map1", + "RootNode.rin_haircap.rin_haircap_1.rin_m_haircap", + "RootNode.rin_haircap.rin_haircap_2.Bitangent", + "RootNode.rin_haircap.rin_haircap_2.SkinWeight_", + "RootNode.rin_haircap.rin_haircap_2.transform", + "RootNode.rin_haircap.rin_haircap_2.Tangent", + "RootNode.rin_haircap.rin_haircap_2.map1", + "RootNode.rin_haircap.rin_haircap_2.rin_m_haircap", + "RootNode.rin_cloth.rin_cloth_1.Col0", + "RootNode.rin_cloth.rin_cloth_1.Bitangent", + "RootNode.rin_cloth.rin_cloth_1.SkinWeight_", + "RootNode.rin_cloth.rin_cloth_1.transform", + "RootNode.rin_cloth.rin_cloth_1.Tangent", + "RootNode.rin_cloth.rin_cloth_1.UVMap", + "RootNode.rin_cloth.rin_cloth_1.rin_m_cloth", + "RootNode.rin_cloth.rin_cloth_2.Col0", + "RootNode.rin_cloth.rin_cloth_2.Bitangent", + "RootNode.rin_cloth.rin_cloth_2.SkinWeight_", + "RootNode.rin_cloth.rin_cloth_2.transform", + "RootNode.rin_cloth.rin_cloth_2.Tangent", + "RootNode.rin_cloth.rin_cloth_2.UVMap", + "RootNode.rin_cloth.rin_cloth_2.rin_m_cloth", + "RootNode.rin_leather.rin_leather_1.Col0", + "RootNode.rin_leather.rin_leather_1.Bitangent", + "RootNode.rin_leather.rin_leather_1.SkinWeight_", + "RootNode.rin_leather.rin_leather_1.transform", + "RootNode.rin_leather.rin_leather_1.Tangent", + "RootNode.rin_leather.rin_leather_1.UVMap", + "RootNode.rin_leather.rin_leather_1.rin_m_leather", + "RootNode.rin_leather.rin_leather_2.Col0", + "RootNode.rin_leather.rin_leather_2.Bitangent", + "RootNode.rin_leather.rin_leather_2.SkinWeight_", + "RootNode.rin_leather.rin_leather_2.transform", + "RootNode.rin_leather.rin_leather_2.Tangent", + "RootNode.rin_leather.rin_leather_2.UVMap", + "RootNode.rin_leather.rin_leather_2.rin_m_leather", + "RootNode.rin_armor.rin_armor_1.Col0", + "RootNode.rin_armor.rin_armor_1.Bitangent", + "RootNode.rin_armor.rin_armor_1.SkinWeight_", + "RootNode.rin_armor.rin_armor_1.transform", + "RootNode.rin_armor.rin_armor_1.Tangent", + "RootNode.rin_armor.rin_armor_1.UVMap", + "RootNode.rin_armor.rin_armor_1.rin_m_armor", + "RootNode.rin_armor.rin_armor_2.Col0", + "RootNode.rin_armor.rin_armor_2.Bitangent", + "RootNode.rin_armor.rin_armor_2.SkinWeight_", + "RootNode.rin_armor.rin_armor_2.transform", + "RootNode.rin_armor.rin_armor_2.Tangent", + "RootNode.rin_armor.rin_armor_2.UVMap", + "RootNode.rin_armor.rin_armor_2.rin_m_armor", + "RootNode.rin_hands.rin_hands_1.Col0", + "RootNode.rin_hands.rin_hands_1.Bitangent", + "RootNode.rin_hands.rin_hands_1.SkinWeight_", + "RootNode.rin_hands.rin_hands_1.transform", + "RootNode.rin_hands.rin_hands_1.Tangent", + "RootNode.rin_hands.rin_hands_1.UVMap", + "RootNode.rin_hands.rin_hands_1.rin_m_hands", + "RootNode.rin_hands.rin_hands_2.Col0", + "RootNode.rin_hands.rin_hands_2.Bitangent", + "RootNode.rin_hands.rin_hands_2.SkinWeight_", + "RootNode.rin_hands.rin_hands_2.transform", + "RootNode.rin_hands.rin_hands_2.Tangent", + "RootNode.rin_hands.rin_hands_2.UVMap", + "RootNode.rin_hands.rin_hands_2.rin_m_hands", + "RootNode.rin_props.rin_props_1.Bitangent", + "RootNode.rin_props.rin_props_1.SkinWeight_", + "RootNode.rin_props.rin_props_1.transform", + "RootNode.rin_props.rin_props_1.Tangent", + "RootNode.rin_props.rin_props_1.UVMap", + "RootNode.rin_props.rin_props_1.map1", + "RootNode.rin_props.rin_props_1.rin_m_props", + "RootNode.rin_props.rin_props_2.Bitangent", + "RootNode.rin_props.rin_props_2.SkinWeight_", + "RootNode.rin_props.rin_props_2.transform", + "RootNode.rin_props.rin_props_2.Tangent", + "RootNode.rin_props.rin_props_2.UVMap", + "RootNode.rin_props.rin_props_2.map1", + "RootNode.rin_props.rin_props_2.rin_m_props", + "RootNode.rin_teeth_low.rin_teeth_low_1.Bitangent", + "RootNode.rin_teeth_low.rin_teeth_low_1.SkinWeight_", + "RootNode.rin_teeth_low.rin_teeth_low_1.transform", + "RootNode.rin_teeth_low.rin_teeth_low_1.Tangent", + "RootNode.rin_teeth_low.rin_teeth_low_1.map1", + "RootNode.rin_teeth_low.rin_teeth_low_1.rin_m_mouth", + "RootNode.rin_teeth_low.rin_teeth_low_2.Bitangent", + "RootNode.rin_teeth_low.rin_teeth_low_2.SkinWeight_", + "RootNode.rin_teeth_low.rin_teeth_low_2.transform", + "RootNode.rin_teeth_low.rin_teeth_low_2.Tangent", + "RootNode.rin_teeth_low.rin_teeth_low_2.map1", + "RootNode.rin_teeth_low.rin_teeth_low_2.rin_m_mouth", + "RootNode.rin_teeth_up.rin_teeth_up_1.Bitangent", + "RootNode.rin_teeth_up.rin_teeth_up_1.SkinWeight_", + "RootNode.rin_teeth_up.rin_teeth_up_1.transform", + "RootNode.rin_teeth_up.rin_teeth_up_1.Tangent", + "RootNode.rin_teeth_up.rin_teeth_up_1.map1", + "RootNode.rin_teeth_up.rin_teeth_up_1.rin_m_mouth", + "RootNode.rin_teeth_up.rin_teeth_up_2.Bitangent", + "RootNode.rin_teeth_up.rin_teeth_up_2.SkinWeight_", + "RootNode.rin_teeth_up.rin_teeth_up_2.transform", + "RootNode.rin_teeth_up.rin_teeth_up_2.Tangent", + "RootNode.rin_teeth_up.rin_teeth_up_2.map1", + "RootNode.rin_teeth_up.rin_teeth_up_2.rin_m_mouth", + "RootNode.rin_tongue.rin_tongue_1.Bitangent", + "RootNode.rin_tongue.rin_tongue_1.SkinWeight_", + "RootNode.rin_tongue.rin_tongue_1.transform", + "RootNode.rin_tongue.rin_tongue_1.Tangent", + "RootNode.rin_tongue.rin_tongue_1.map1", + "RootNode.rin_tongue.rin_tongue_1.rin_m_mouth", + "RootNode.rin_tongue.rin_tongue_2.Bitangent", + "RootNode.rin_tongue.rin_tongue_2.SkinWeight_", + "RootNode.rin_tongue.rin_tongue_2.transform", + "RootNode.rin_tongue.rin_tongue_2.Tangent", + "RootNode.rin_tongue.rin_tongue_2.map1", + "RootNode.rin_tongue.rin_tongue_2.rin_m_mouth", + "RootNode.rin_face.rin_face_1.Bitangent", + "RootNode.rin_face.rin_face_1.SkinWeight_", + "RootNode.rin_face.rin_face_1.transform", + "RootNode.rin_face.rin_face_1.Tangent", + "RootNode.rin_face.rin_face_1.map1", + "RootNode.rin_face.rin_face_1.rin_m_face", + "RootNode.rin_face.rin_face_2.Bitangent", + "RootNode.rin_face.rin_face_2.SkinWeight_", + "RootNode.rin_face.rin_face_2.transform", + "RootNode.rin_face.rin_face_2.Tangent", + "RootNode.rin_face.rin_face_2.map1", + "RootNode.rin_face.rin_face_2.rin_m_face", + "RootNode.rin_armorstraps.rin_armorstraps_1.Bitangent", + "RootNode.rin_armorstraps.rin_armorstraps_1.SkinWeight_", + "RootNode.rin_armorstraps.rin_armorstraps_1.transform", + "RootNode.rin_armorstraps.rin_armorstraps_1.Tangent", + "RootNode.rin_armorstraps.rin_armorstraps_1.map1", + "RootNode.rin_armorstraps.rin_armorstraps_1.rin_m_armor", + "RootNode.rin_armorstraps.rin_armorstraps_2.Bitangent", + "RootNode.rin_armorstraps.rin_armorstraps_2.SkinWeight_", + "RootNode.rin_armorstraps.rin_armorstraps_2.transform", + "RootNode.rin_armorstraps.rin_armorstraps_2.Tangent", + "RootNode.rin_armorstraps.rin_armorstraps_2.map1", + "RootNode.rin_armorstraps.rin_armorstraps_2.rin_m_armor", + "RootNode.rin_hairplanes.rin_hairplanes_1.Bitangent", + "RootNode.rin_hairplanes.rin_hairplanes_1.SkinWeight_", + "RootNode.rin_hairplanes.rin_hairplanes_1.transform", + "RootNode.rin_hairplanes.rin_hairplanes_1.Tangent", + "RootNode.rin_hairplanes.rin_hairplanes_1.map1", + "RootNode.rin_hairplanes.rin_hairplanes_1.rin_m_hairplanes", + "RootNode.rin_hairplanes.rin_hairplanes_2.Bitangent", + "RootNode.rin_hairplanes.rin_hairplanes_2.SkinWeight_", + "RootNode.rin_hairplanes.rin_hairplanes_2.transform", + "RootNode.rin_hairplanes.rin_hairplanes_2.Tangent", + "RootNode.rin_hairplanes.rin_hairplanes_2.map1", + "RootNode.rin_hairplanes.rin_hairplanes_2.rin_m_hairplanes", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.Bitangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.SkinWeight_", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.transform", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.Tangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.map1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.rin_m_eyebrow", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.Bitangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.SkinWeight_", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.transform", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.Tangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.map1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.rin_m_eyebrow", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.Bitangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.SkinWeight_", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.transform", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.Tangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.map1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.rin_m_lashes", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.Bitangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.SkinWeight_", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.transform", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.Tangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.map1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.rin_m_lashes", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.Bitangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.SkinWeight_", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.transform", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.Tangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.map1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.rin_m_eyebrow", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.Bitangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.SkinWeight_", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.transform", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.Tangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.map1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.rin_m_eyebrow", + "RootNode.rin_eyecover.rin_eyecover_1.Bitangent", + "RootNode.rin_eyecover.rin_eyecover_1.SkinWeight_", + "RootNode.rin_eyecover.rin_eyecover_1.transform", + "RootNode.rin_eyecover.rin_eyecover_1.Tangent", + "RootNode.rin_eyecover.rin_eyecover_1.map1", + "RootNode.rin_eyecover.rin_eyecover_1.rin_m_eyecover", + "RootNode.rin_eyecover.rin_eyecover_2.Bitangent", + "RootNode.rin_eyecover.rin_eyecover_2.SkinWeight_", + "RootNode.rin_eyecover.rin_eyecover_2.transform", + "RootNode.rin_eyecover.rin_eyecover_2.Tangent", + "RootNode.rin_eyecover.rin_eyecover_2.map1", + "RootNode.rin_eyecover.rin_eyecover_2.rin_m_eyecover", + "RootNode.rin_facefuzz.rin_facefuzz_1.Bitangent", + "RootNode.rin_facefuzz.rin_facefuzz_1.SkinWeight_", + "RootNode.rin_facefuzz.rin_facefuzz_1.transform", + "RootNode.rin_facefuzz.rin_facefuzz_1.Tangent", + "RootNode.rin_facefuzz.rin_facefuzz_1.map1", + "RootNode.rin_facefuzz.rin_facefuzz_1.rin_m_fuzz", + "RootNode.rin_facefuzz.rin_facefuzz_2.Bitangent", + "RootNode.rin_facefuzz.rin_facefuzz_2.SkinWeight_", + "RootNode.rin_facefuzz.rin_facefuzz_2.transform", + "RootNode.rin_facefuzz.rin_facefuzz_2.Tangent", + "RootNode.rin_facefuzz.rin_facefuzz_2.map1", + "RootNode.rin_facefuzz.rin_facefuzz_2.rin_m_fuzz", + "RootNode.rin_haircards.rin_haircards_1.Bitangent", + "RootNode.rin_haircards.rin_haircards_1.SkinWeight_", + "RootNode.rin_haircards.rin_haircards_1.transform", + "RootNode.rin_haircards.rin_haircards_1.Tangent", + "RootNode.rin_haircards.rin_haircards_1.map1", + "RootNode.rin_haircards.rin_haircards_1.rin_m_haircards", + "RootNode.rin_haircards.rin_haircards_2.Bitangent", + "RootNode.rin_haircards.rin_haircards_2.SkinWeight_", + "RootNode.rin_haircards.rin_haircards_2.transform", + "RootNode.rin_haircards.rin_haircards_2.Tangent", + "RootNode.rin_haircards.rin_haircards_2.map1", + "RootNode.rin_haircards.rin_haircards_2.rin_m_haircards", + "RootNode.rin_lash_01.rin_lash_01_1.Bitangent", + "RootNode.rin_lash_01.rin_lash_01_1.SkinWeight_", + "RootNode.rin_lash_01.rin_lash_01_1.transform", + "RootNode.rin_lash_01.rin_lash_01_1.Tangent", + "RootNode.rin_lash_01.rin_lash_01_1.map1", + "RootNode.rin_lash_01.rin_lash_01_1.rin_m_lashes", + "RootNode.rin_lash_01.rin_lash_01_2.Bitangent", + "RootNode.rin_lash_01.rin_lash_01_2.SkinWeight_", + "RootNode.rin_lash_01.rin_lash_01_2.transform", + "RootNode.rin_lash_01.rin_lash_01_2.Tangent", + "RootNode.rin_lash_01.rin_lash_01_2.map1", + "RootNode.rin_lash_01.rin_lash_01_2.rin_m_lashes", + "RootNode.rin_lash_02.rin_lash_02_1.Bitangent", + "RootNode.rin_lash_02.rin_lash_02_1.SkinWeight_", + "RootNode.rin_lash_02.rin_lash_02_1.transform", + "RootNode.rin_lash_02.rin_lash_02_1.Tangent", + "RootNode.rin_lash_02.rin_lash_02_1.map1", + "RootNode.rin_lash_02.rin_lash_02_1.rin_m_lashes", + "RootNode.rin_lash_02.rin_lash_02_2.Bitangent", + "RootNode.rin_lash_02.rin_lash_02_2.SkinWeight_", + "RootNode.rin_lash_02.rin_lash_02_2.transform", + "RootNode.rin_lash_02.rin_lash_02_2.Tangent", + "RootNode.rin_lash_02.rin_lash_02_2.map1", + "RootNode.rin_lash_02.rin_lash_02_2.rin_m_lashes", + "RootNode.rin_lash_03.rin_lash_03_1.Bitangent", + "RootNode.rin_lash_03.rin_lash_03_1.SkinWeight_", + "RootNode.rin_lash_03.rin_lash_03_1.transform", + "RootNode.rin_lash_03.rin_lash_03_1.Tangent", + "RootNode.rin_lash_03.rin_lash_03_1.map1", + "RootNode.rin_lash_03.rin_lash_03_1.rin_m_lashes", + "RootNode.rin_lash_03.rin_lash_03_2.Bitangent", + "RootNode.rin_lash_03.rin_lash_03_2.SkinWeight_", + "RootNode.rin_lash_03.rin_lash_03_2.transform", + "RootNode.rin_lash_03.rin_lash_03_2.Tangent", + "RootNode.rin_lash_03.rin_lash_03_2.map1", + "RootNode.rin_lash_03.rin_lash_03_2.rin_m_lashes", + "RootNode.rin_eyewetness.rin_eyewetness_1.Bitangent", + "RootNode.rin_eyewetness.rin_eyewetness_1.SkinWeight_", + "RootNode.rin_eyewetness.rin_eyewetness_1.transform", + "RootNode.rin_eyewetness.rin_eyewetness_1.Tangent", + "RootNode.rin_eyewetness.rin_eyewetness_1.map1", + "RootNode.rin_eyewetness.rin_eyewetness_1.rin_m_eyewetness", + "RootNode.rin_eyewetness.rin_eyewetness_2.Bitangent", + "RootNode.rin_eyewetness.rin_eyewetness_2.SkinWeight_", + "RootNode.rin_eyewetness.rin_eyewetness_2.transform", + "RootNode.rin_eyewetness.rin_eyewetness_2.Tangent", + "RootNode.rin_eyewetness.rin_eyewetness_2.map1", + "RootNode.rin_eyewetness.rin_eyewetness_2.rin_m_eyewetness", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.Bitangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.SkinWeight_", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.transform", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.Tangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.map1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.rin_m_eyebrow", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.Bitangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.SkinWeight_", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.transform", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.Tangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.map1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.rin_m_eyebrow", + "RootNode.rin_tearduct.rin_tearduct_1.Bitangent", + "RootNode.rin_tearduct.rin_tearduct_1.SkinWeight_", + "RootNode.rin_tearduct.rin_tearduct_1.transform", + "RootNode.rin_tearduct.rin_tearduct_1.Tangent", + "RootNode.rin_tearduct.rin_tearduct_1.map1", + "RootNode.rin_tearduct.rin_tearduct_1.rin_m_tearduct", + "RootNode.rin_tearduct.rin_tearduct_2.Bitangent", + "RootNode.rin_tearduct.rin_tearduct_2.SkinWeight_", + "RootNode.rin_tearduct.rin_tearduct_2.transform", + "RootNode.rin_tearduct.rin_tearduct_2.Tangent", + "RootNode.rin_tearduct.rin_tearduct_2.map1", + "RootNode.rin_tearduct.rin_tearduct_2.rin_m_tearduct", + "RootNode.root.C_pelvis_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT", + "RootNode.root.C_pelvis_JNT.C_legArmor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_sword_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_leg_twist_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_leg_twist_JNT", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.L_ribbon_02_JNT", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.R_ribbon_02_JNT", + "RootNode.root.C_pelvis_JNT.C_legArmor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_sword_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_knee_twist_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_leg_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_knee_twist_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_leg_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.L_ribbon_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.R_ribbon_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.L_toe_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_knee_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.R_toe_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_knee_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neckCollar_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_02_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.L_toe_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.R_toe_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_armPit_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_armPit_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neckCollar_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_arm_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_armBulge_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.L_tassleLoop_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_armPit_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_throat_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_arm_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_armBulge_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.R_tassleLoop_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_armPit_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.C_hood_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_elbow_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_arm_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_armBulge_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.L_tassle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.L_tassleLoop_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_corrugator_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_noseBridge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_zygomatic_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekBone_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_ear_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_lower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_upper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_levator_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipLevator_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_upper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_lower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekBone_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_zygomatic_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_ear_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_slide_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_fold_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_fold_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_bulge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_bulge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_throat_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_elbow_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_arm_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_armBulge_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.R_tassle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.R_tassleLoop_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.C_hood_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_elbow_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.L_tassle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_corrugator_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_noseBridge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_zygomatic_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekBone_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_ear_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_lower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_upper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_levator_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipLevator_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_mentalis_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_depressor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_depressor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lip_nasolabial_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_mouth_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lip_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lip_nasolabial_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_mouth_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_jaw_clench_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_jaw_clench_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_zygomatic_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_zygomatic_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_upper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_lower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekBone_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_zygomatic_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_ear_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_slide_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_fold_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_fold_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_bulge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_bulge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_elbow_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.R_tassle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_mentalis_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_depressor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_depressor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lip_nasolabial_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_mouth_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lip_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lip_nasolabial_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_mouth_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_jaw_clench_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_jaw_clench_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_zygomatic_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_zygomatic_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.L_thumb_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.C_tongue_tip_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.R_thumb_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.L_thumb_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.L_index_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.L_middle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.L_ring_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.L_pinky_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.C_tongue_tip_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.R_thumb_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.R_index_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.R_middle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.R_ring_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.R_pinky_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.L_index_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.L_middle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.L_ring_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.L_pinky_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.R_index_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.R_middle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.R_ring_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.R_pinky_03_JNT.transform" + ] + } + }, + { + "$type": "{07B356B7-3635-40B5-878A-FAC4EFD5AD86} MeshGroup", + "name": "RinMM", + "nodeSelectionList": { + "selectedNodes": [ + "RootNode", + "RootNode.rin_eyeballs", + "RootNode.rin_haircap", + "RootNode.rin_cloth", + "RootNode.rin_leather", + "RootNode.rin_armor", + "RootNode.rin_hands", + "RootNode.rin_props", + "RootNode.rin_teeth_low", + "RootNode.rin_teeth_up", + "RootNode.rin_tongue", + "RootNode.rin_face", + "RootNode.rin_armorstraps", + "RootNode.rin_hairplanes", + "RootNode.rin_eyebrows_top", + "RootNode.rin_eyelashes_top", + "RootNode.rin_eyelashes_lower", + "RootNode.rin_eyecover", + "RootNode.rin_facefuzz", + "RootNode.rin_haircards", + "RootNode.rin_lash_01", + "RootNode.rin_lash_02", + "RootNode.rin_lash_03", + "RootNode.rin_eyewetness", + "RootNode.rin_eyebrows_lower", + "RootNode.rin_tearduct", + "RootNode.root", + "RootNode.rin_eyeballs.rin_eyeballs_1", + "RootNode.rin_eyeballs.rin_eyeballs_2", + "RootNode.rin_haircap.rin_haircap_1", + "RootNode.rin_haircap.rin_haircap_2", + "RootNode.rin_cloth.rin_cloth_1", + "RootNode.rin_cloth.rin_cloth_2", + "RootNode.rin_leather.rin_leather_1", + "RootNode.rin_leather.rin_leather_2", + "RootNode.rin_armor.rin_armor_1", + "RootNode.rin_armor.rin_armor_2", + "RootNode.rin_hands.rin_hands_1", + "RootNode.rin_hands.rin_hands_2", + "RootNode.rin_props.rin_props_1", + "RootNode.rin_props.rin_props_2", + "RootNode.rin_teeth_low.rin_teeth_low_1", + "RootNode.rin_teeth_low.rin_teeth_low_2", + "RootNode.rin_teeth_up.rin_teeth_up_1", + "RootNode.rin_teeth_up.rin_teeth_up_2", + "RootNode.rin_tongue.rin_tongue_1", + "RootNode.rin_tongue.rin_tongue_2", + "RootNode.rin_face.rin_face_1", + "RootNode.rin_face.rin_face_2", + "RootNode.rin_armorstraps.rin_armorstraps_1", + "RootNode.rin_armorstraps.rin_armorstraps_2", + "RootNode.rin_hairplanes.rin_hairplanes_1", + "RootNode.rin_hairplanes.rin_hairplanes_2", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2", + "RootNode.rin_eyecover.rin_eyecover_1", + "RootNode.rin_eyecover.rin_eyecover_2", + "RootNode.rin_facefuzz.rin_facefuzz_1", + "RootNode.rin_facefuzz.rin_facefuzz_2", + "RootNode.rin_haircards.rin_haircards_1", + "RootNode.rin_haircards.rin_haircards_2", + "RootNode.rin_lash_01.rin_lash_01_1", + "RootNode.rin_lash_01.rin_lash_01_2", + "RootNode.rin_lash_02.rin_lash_02_1", + "RootNode.rin_lash_02.rin_lash_02_2", + "RootNode.rin_lash_03.rin_lash_03_1", + "RootNode.rin_lash_03.rin_lash_03_2", + "RootNode.rin_eyewetness.rin_eyewetness_1", + "RootNode.rin_eyewetness.rin_eyewetness_2", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2", + "RootNode.rin_tearduct.rin_tearduct_1", + "RootNode.rin_tearduct.rin_tearduct_2", + "RootNode.root.C_pelvis_JNT", + "RootNode.rin_eyeballs.rin_eyeballs_1.Bitangent", + "RootNode.rin_eyeballs.rin_eyeballs_1.SkinWeight_", + "RootNode.rin_eyeballs.rin_eyeballs_1.transform", + "RootNode.rin_eyeballs.rin_eyeballs_1.Tangent", + "RootNode.rin_eyeballs.rin_eyeballs_1.map1", + "RootNode.rin_eyeballs.rin_eyeballs_1.rin_m_eyeballs", + "RootNode.rin_eyeballs.rin_eyeballs_2.Bitangent", + "RootNode.rin_eyeballs.rin_eyeballs_2.SkinWeight_", + "RootNode.rin_eyeballs.rin_eyeballs_2.transform", + "RootNode.rin_eyeballs.rin_eyeballs_2.Tangent", + "RootNode.rin_eyeballs.rin_eyeballs_2.map1", + "RootNode.rin_eyeballs.rin_eyeballs_2.rin_m_eyeballs", + "RootNode.rin_haircap.rin_haircap_1.Bitangent", + "RootNode.rin_haircap.rin_haircap_1.SkinWeight_", + "RootNode.rin_haircap.rin_haircap_1.transform", + "RootNode.rin_haircap.rin_haircap_1.Tangent", + "RootNode.rin_haircap.rin_haircap_1.map1", + "RootNode.rin_haircap.rin_haircap_1.rin_m_haircap", + "RootNode.rin_haircap.rin_haircap_2.Bitangent", + "RootNode.rin_haircap.rin_haircap_2.SkinWeight_", + "RootNode.rin_haircap.rin_haircap_2.transform", + "RootNode.rin_haircap.rin_haircap_2.Tangent", + "RootNode.rin_haircap.rin_haircap_2.map1", + "RootNode.rin_haircap.rin_haircap_2.rin_m_haircap", + "RootNode.rin_cloth.rin_cloth_1.Col0", + "RootNode.rin_cloth.rin_cloth_1.Bitangent", + "RootNode.rin_cloth.rin_cloth_1.SkinWeight_", + "RootNode.rin_cloth.rin_cloth_1.transform", + "RootNode.rin_cloth.rin_cloth_1.Tangent", + "RootNode.rin_cloth.rin_cloth_1.UVMap", + "RootNode.rin_cloth.rin_cloth_1.rin_m_cloth", + "RootNode.rin_cloth.rin_cloth_2.Col0", + "RootNode.rin_cloth.rin_cloth_2.Bitangent", + "RootNode.rin_cloth.rin_cloth_2.SkinWeight_", + "RootNode.rin_cloth.rin_cloth_2.transform", + "RootNode.rin_cloth.rin_cloth_2.Tangent", + "RootNode.rin_cloth.rin_cloth_2.UVMap", + "RootNode.rin_cloth.rin_cloth_2.rin_m_cloth", + "RootNode.rin_leather.rin_leather_1.Col0", + "RootNode.rin_leather.rin_leather_1.Bitangent", + "RootNode.rin_leather.rin_leather_1.SkinWeight_", + "RootNode.rin_leather.rin_leather_1.transform", + "RootNode.rin_leather.rin_leather_1.Tangent", + "RootNode.rin_leather.rin_leather_1.UVMap", + "RootNode.rin_leather.rin_leather_1.rin_m_leather", + "RootNode.rin_leather.rin_leather_2.Col0", + "RootNode.rin_leather.rin_leather_2.Bitangent", + "RootNode.rin_leather.rin_leather_2.SkinWeight_", + "RootNode.rin_leather.rin_leather_2.transform", + "RootNode.rin_leather.rin_leather_2.Tangent", + "RootNode.rin_leather.rin_leather_2.UVMap", + "RootNode.rin_leather.rin_leather_2.rin_m_leather", + "RootNode.rin_armor.rin_armor_1.Col0", + "RootNode.rin_armor.rin_armor_1.Bitangent", + "RootNode.rin_armor.rin_armor_1.SkinWeight_", + "RootNode.rin_armor.rin_armor_1.transform", + "RootNode.rin_armor.rin_armor_1.Tangent", + "RootNode.rin_armor.rin_armor_1.UVMap", + "RootNode.rin_armor.rin_armor_1.rin_m_armor", + "RootNode.rin_armor.rin_armor_2.Col0", + "RootNode.rin_armor.rin_armor_2.Bitangent", + "RootNode.rin_armor.rin_armor_2.SkinWeight_", + "RootNode.rin_armor.rin_armor_2.transform", + "RootNode.rin_armor.rin_armor_2.Tangent", + "RootNode.rin_armor.rin_armor_2.UVMap", + "RootNode.rin_armor.rin_armor_2.rin_m_armor", + "RootNode.rin_hands.rin_hands_1.Col0", + "RootNode.rin_hands.rin_hands_1.Bitangent", + "RootNode.rin_hands.rin_hands_1.SkinWeight_", + "RootNode.rin_hands.rin_hands_1.transform", + "RootNode.rin_hands.rin_hands_1.Tangent", + "RootNode.rin_hands.rin_hands_1.UVMap", + "RootNode.rin_hands.rin_hands_1.rin_m_hands", + "RootNode.rin_hands.rin_hands_2.Col0", + "RootNode.rin_hands.rin_hands_2.Bitangent", + "RootNode.rin_hands.rin_hands_2.SkinWeight_", + "RootNode.rin_hands.rin_hands_2.transform", + "RootNode.rin_hands.rin_hands_2.Tangent", + "RootNode.rin_hands.rin_hands_2.UVMap", + "RootNode.rin_hands.rin_hands_2.rin_m_hands", + "RootNode.rin_props.rin_props_1.Bitangent", + "RootNode.rin_props.rin_props_1.SkinWeight_", + "RootNode.rin_props.rin_props_1.transform", + "RootNode.rin_props.rin_props_1.Tangent", + "RootNode.rin_props.rin_props_1.UVMap", + "RootNode.rin_props.rin_props_1.map1", + "RootNode.rin_props.rin_props_1.rin_m_props", + "RootNode.rin_props.rin_props_2.Bitangent", + "RootNode.rin_props.rin_props_2.SkinWeight_", + "RootNode.rin_props.rin_props_2.transform", + "RootNode.rin_props.rin_props_2.Tangent", + "RootNode.rin_props.rin_props_2.UVMap", + "RootNode.rin_props.rin_props_2.map1", + "RootNode.rin_props.rin_props_2.rin_m_props", + "RootNode.rin_teeth_low.rin_teeth_low_1.Bitangent", + "RootNode.rin_teeth_low.rin_teeth_low_1.SkinWeight_", + "RootNode.rin_teeth_low.rin_teeth_low_1.transform", + "RootNode.rin_teeth_low.rin_teeth_low_1.Tangent", + "RootNode.rin_teeth_low.rin_teeth_low_1.map1", + "RootNode.rin_teeth_low.rin_teeth_low_1.rin_m_mouth", + "RootNode.rin_teeth_low.rin_teeth_low_2.Bitangent", + "RootNode.rin_teeth_low.rin_teeth_low_2.SkinWeight_", + "RootNode.rin_teeth_low.rin_teeth_low_2.transform", + "RootNode.rin_teeth_low.rin_teeth_low_2.Tangent", + "RootNode.rin_teeth_low.rin_teeth_low_2.map1", + "RootNode.rin_teeth_low.rin_teeth_low_2.rin_m_mouth", + "RootNode.rin_teeth_up.rin_teeth_up_1.Bitangent", + "RootNode.rin_teeth_up.rin_teeth_up_1.SkinWeight_", + "RootNode.rin_teeth_up.rin_teeth_up_1.transform", + "RootNode.rin_teeth_up.rin_teeth_up_1.Tangent", + "RootNode.rin_teeth_up.rin_teeth_up_1.map1", + "RootNode.rin_teeth_up.rin_teeth_up_1.rin_m_mouth", + "RootNode.rin_teeth_up.rin_teeth_up_2.Bitangent", + "RootNode.rin_teeth_up.rin_teeth_up_2.SkinWeight_", + "RootNode.rin_teeth_up.rin_teeth_up_2.transform", + "RootNode.rin_teeth_up.rin_teeth_up_2.Tangent", + "RootNode.rin_teeth_up.rin_teeth_up_2.map1", + "RootNode.rin_teeth_up.rin_teeth_up_2.rin_m_mouth", + "RootNode.rin_tongue.rin_tongue_1.Bitangent", + "RootNode.rin_tongue.rin_tongue_1.SkinWeight_", + "RootNode.rin_tongue.rin_tongue_1.transform", + "RootNode.rin_tongue.rin_tongue_1.Tangent", + "RootNode.rin_tongue.rin_tongue_1.map1", + "RootNode.rin_tongue.rin_tongue_1.rin_m_mouth", + "RootNode.rin_tongue.rin_tongue_2.Bitangent", + "RootNode.rin_tongue.rin_tongue_2.SkinWeight_", + "RootNode.rin_tongue.rin_tongue_2.transform", + "RootNode.rin_tongue.rin_tongue_2.Tangent", + "RootNode.rin_tongue.rin_tongue_2.map1", + "RootNode.rin_tongue.rin_tongue_2.rin_m_mouth", + "RootNode.rin_face.rin_face_1.Bitangent", + "RootNode.rin_face.rin_face_1.SkinWeight_", + "RootNode.rin_face.rin_face_1.transform", + "RootNode.rin_face.rin_face_1.Tangent", + "RootNode.rin_face.rin_face_1.map1", + "RootNode.rin_face.rin_face_1.rin_m_face", + "RootNode.rin_face.rin_face_2.Bitangent", + "RootNode.rin_face.rin_face_2.SkinWeight_", + "RootNode.rin_face.rin_face_2.transform", + "RootNode.rin_face.rin_face_2.Tangent", + "RootNode.rin_face.rin_face_2.map1", + "RootNode.rin_face.rin_face_2.rin_m_face", + "RootNode.rin_armorstraps.rin_armorstraps_1.Bitangent", + "RootNode.rin_armorstraps.rin_armorstraps_1.SkinWeight_", + "RootNode.rin_armorstraps.rin_armorstraps_1.transform", + "RootNode.rin_armorstraps.rin_armorstraps_1.Tangent", + "RootNode.rin_armorstraps.rin_armorstraps_1.map1", + "RootNode.rin_armorstraps.rin_armorstraps_1.rin_m_armor", + "RootNode.rin_armorstraps.rin_armorstraps_2.Bitangent", + "RootNode.rin_armorstraps.rin_armorstraps_2.SkinWeight_", + "RootNode.rin_armorstraps.rin_armorstraps_2.transform", + "RootNode.rin_armorstraps.rin_armorstraps_2.Tangent", + "RootNode.rin_armorstraps.rin_armorstraps_2.map1", + "RootNode.rin_armorstraps.rin_armorstraps_2.rin_m_armor", + "RootNode.rin_hairplanes.rin_hairplanes_1.Bitangent", + "RootNode.rin_hairplanes.rin_hairplanes_1.SkinWeight_", + "RootNode.rin_hairplanes.rin_hairplanes_1.transform", + "RootNode.rin_hairplanes.rin_hairplanes_1.Tangent", + "RootNode.rin_hairplanes.rin_hairplanes_1.map1", + "RootNode.rin_hairplanes.rin_hairplanes_1.rin_m_hairplanes", + "RootNode.rin_hairplanes.rin_hairplanes_2.Bitangent", + "RootNode.rin_hairplanes.rin_hairplanes_2.SkinWeight_", + "RootNode.rin_hairplanes.rin_hairplanes_2.transform", + "RootNode.rin_hairplanes.rin_hairplanes_2.Tangent", + "RootNode.rin_hairplanes.rin_hairplanes_2.map1", + "RootNode.rin_hairplanes.rin_hairplanes_2.rin_m_hairplanes", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.Bitangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.SkinWeight_", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.transform", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.Tangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.map1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_1.rin_m_eyebrow", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.Bitangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.SkinWeight_", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.transform", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.Tangent", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.map1", + "RootNode.rin_eyebrows_top.rin_eyebrows_top_2.rin_m_eyebrow", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.Bitangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.SkinWeight_", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.transform", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.Tangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.map1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_1.rin_m_lashes", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.Bitangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.SkinWeight_", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.transform", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.Tangent", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.map1", + "RootNode.rin_eyelashes_top.rin_eyelashes_top_2.rin_m_lashes", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.Bitangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.SkinWeight_", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.transform", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.Tangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.map1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_1.rin_m_eyebrow", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.Bitangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.SkinWeight_", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.transform", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.Tangent", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.map1", + "RootNode.rin_eyelashes_lower.rin_eyelashes_lower_2.rin_m_eyebrow", + "RootNode.rin_eyecover.rin_eyecover_1.Bitangent", + "RootNode.rin_eyecover.rin_eyecover_1.SkinWeight_", + "RootNode.rin_eyecover.rin_eyecover_1.transform", + "RootNode.rin_eyecover.rin_eyecover_1.Tangent", + "RootNode.rin_eyecover.rin_eyecover_1.map1", + "RootNode.rin_eyecover.rin_eyecover_1.rin_m_eyecover", + "RootNode.rin_eyecover.rin_eyecover_2.Bitangent", + "RootNode.rin_eyecover.rin_eyecover_2.SkinWeight_", + "RootNode.rin_eyecover.rin_eyecover_2.transform", + "RootNode.rin_eyecover.rin_eyecover_2.Tangent", + "RootNode.rin_eyecover.rin_eyecover_2.map1", + "RootNode.rin_eyecover.rin_eyecover_2.rin_m_eyecover", + "RootNode.rin_facefuzz.rin_facefuzz_1.Bitangent", + "RootNode.rin_facefuzz.rin_facefuzz_1.SkinWeight_", + "RootNode.rin_facefuzz.rin_facefuzz_1.transform", + "RootNode.rin_facefuzz.rin_facefuzz_1.Tangent", + "RootNode.rin_facefuzz.rin_facefuzz_1.map1", + "RootNode.rin_facefuzz.rin_facefuzz_1.rin_m_fuzz", + "RootNode.rin_facefuzz.rin_facefuzz_2.Bitangent", + "RootNode.rin_facefuzz.rin_facefuzz_2.SkinWeight_", + "RootNode.rin_facefuzz.rin_facefuzz_2.transform", + "RootNode.rin_facefuzz.rin_facefuzz_2.Tangent", + "RootNode.rin_facefuzz.rin_facefuzz_2.map1", + "RootNode.rin_facefuzz.rin_facefuzz_2.rin_m_fuzz", + "RootNode.rin_haircards.rin_haircards_1.Bitangent", + "RootNode.rin_haircards.rin_haircards_1.SkinWeight_", + "RootNode.rin_haircards.rin_haircards_1.transform", + "RootNode.rin_haircards.rin_haircards_1.Tangent", + "RootNode.rin_haircards.rin_haircards_1.map1", + "RootNode.rin_haircards.rin_haircards_1.rin_m_haircards", + "RootNode.rin_haircards.rin_haircards_2.Bitangent", + "RootNode.rin_haircards.rin_haircards_2.SkinWeight_", + "RootNode.rin_haircards.rin_haircards_2.transform", + "RootNode.rin_haircards.rin_haircards_2.Tangent", + "RootNode.rin_haircards.rin_haircards_2.map1", + "RootNode.rin_haircards.rin_haircards_2.rin_m_haircards", + "RootNode.rin_lash_01.rin_lash_01_1.Bitangent", + "RootNode.rin_lash_01.rin_lash_01_1.SkinWeight_", + "RootNode.rin_lash_01.rin_lash_01_1.transform", + "RootNode.rin_lash_01.rin_lash_01_1.Tangent", + "RootNode.rin_lash_01.rin_lash_01_1.map1", + "RootNode.rin_lash_01.rin_lash_01_1.rin_m_lashes", + "RootNode.rin_lash_01.rin_lash_01_2.Bitangent", + "RootNode.rin_lash_01.rin_lash_01_2.SkinWeight_", + "RootNode.rin_lash_01.rin_lash_01_2.transform", + "RootNode.rin_lash_01.rin_lash_01_2.Tangent", + "RootNode.rin_lash_01.rin_lash_01_2.map1", + "RootNode.rin_lash_01.rin_lash_01_2.rin_m_lashes", + "RootNode.rin_lash_02.rin_lash_02_1.Bitangent", + "RootNode.rin_lash_02.rin_lash_02_1.SkinWeight_", + "RootNode.rin_lash_02.rin_lash_02_1.transform", + "RootNode.rin_lash_02.rin_lash_02_1.Tangent", + "RootNode.rin_lash_02.rin_lash_02_1.map1", + "RootNode.rin_lash_02.rin_lash_02_1.rin_m_lashes", + "RootNode.rin_lash_02.rin_lash_02_2.Bitangent", + "RootNode.rin_lash_02.rin_lash_02_2.SkinWeight_", + "RootNode.rin_lash_02.rin_lash_02_2.transform", + "RootNode.rin_lash_02.rin_lash_02_2.Tangent", + "RootNode.rin_lash_02.rin_lash_02_2.map1", + "RootNode.rin_lash_02.rin_lash_02_2.rin_m_lashes", + "RootNode.rin_lash_03.rin_lash_03_1.Bitangent", + "RootNode.rin_lash_03.rin_lash_03_1.SkinWeight_", + "RootNode.rin_lash_03.rin_lash_03_1.transform", + "RootNode.rin_lash_03.rin_lash_03_1.Tangent", + "RootNode.rin_lash_03.rin_lash_03_1.map1", + "RootNode.rin_lash_03.rin_lash_03_1.rin_m_lashes", + "RootNode.rin_lash_03.rin_lash_03_2.Bitangent", + "RootNode.rin_lash_03.rin_lash_03_2.SkinWeight_", + "RootNode.rin_lash_03.rin_lash_03_2.transform", + "RootNode.rin_lash_03.rin_lash_03_2.Tangent", + "RootNode.rin_lash_03.rin_lash_03_2.map1", + "RootNode.rin_lash_03.rin_lash_03_2.rin_m_lashes", + "RootNode.rin_eyewetness.rin_eyewetness_1.Bitangent", + "RootNode.rin_eyewetness.rin_eyewetness_1.SkinWeight_", + "RootNode.rin_eyewetness.rin_eyewetness_1.transform", + "RootNode.rin_eyewetness.rin_eyewetness_1.Tangent", + "RootNode.rin_eyewetness.rin_eyewetness_1.map1", + "RootNode.rin_eyewetness.rin_eyewetness_1.rin_m_eyewetness", + "RootNode.rin_eyewetness.rin_eyewetness_2.Bitangent", + "RootNode.rin_eyewetness.rin_eyewetness_2.SkinWeight_", + "RootNode.rin_eyewetness.rin_eyewetness_2.transform", + "RootNode.rin_eyewetness.rin_eyewetness_2.Tangent", + "RootNode.rin_eyewetness.rin_eyewetness_2.map1", + "RootNode.rin_eyewetness.rin_eyewetness_2.rin_m_eyewetness", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.Bitangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.SkinWeight_", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.transform", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.Tangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.map1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_1.rin_m_eyebrow", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.Bitangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.SkinWeight_", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.transform", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.Tangent", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.map1", + "RootNode.rin_eyebrows_lower.rin_eyebrows_lower_2.rin_m_eyebrow", + "RootNode.rin_tearduct.rin_tearduct_1.Bitangent", + "RootNode.rin_tearduct.rin_tearduct_1.SkinWeight_", + "RootNode.rin_tearduct.rin_tearduct_1.transform", + "RootNode.rin_tearduct.rin_tearduct_1.Tangent", + "RootNode.rin_tearduct.rin_tearduct_1.map1", + "RootNode.rin_tearduct.rin_tearduct_1.rin_m_tearduct", + "RootNode.rin_tearduct.rin_tearduct_2.Bitangent", + "RootNode.rin_tearduct.rin_tearduct_2.SkinWeight_", + "RootNode.rin_tearduct.rin_tearduct_2.transform", + "RootNode.rin_tearduct.rin_tearduct_2.Tangent", + "RootNode.rin_tearduct.rin_tearduct_2.map1", + "RootNode.rin_tearduct.rin_tearduct_2.rin_m_tearduct", + "RootNode.root.C_pelvis_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT", + "RootNode.root.C_pelvis_JNT.C_legArmor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_sword_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_leg_twist_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_leg_twist_JNT", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.L_ribbon_02_JNT", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.R_ribbon_02_JNT", + "RootNode.root.C_pelvis_JNT.C_legArmor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_sword_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_knee_twist_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_leg_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_knee_twist_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_leg_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_ribbon_01_JNT.L_ribbon_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_ribbon_01_JNT.R_ribbon_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.transform", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.L_toe_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_knee_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.R_toe_JNT", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_knee_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neckCollar_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_02_JNT", + "RootNode.root.C_pelvis_JNT.L_leg_JNT.L_knee_JNT.L_foot_JNT.L_toe_JNT.transform", + "RootNode.root.C_pelvis_JNT.R_leg_JNT.R_knee_JNT.R_foot_JNT.R_toe_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_armPit_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_armPit_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neckCollar_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_neckCollar_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_neckCollar_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_arm_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_armBulge_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.L_tassleLoop_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_armPit_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_throat_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_arm_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_armBulge_corr_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.R_tassleLoop_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_armPit_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.C_hood_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_elbow_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_arm_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_armBulge_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.L_tassle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassleLoop_01_JNT.L_tassleLoop_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_corrugator_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_noseBridge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_zygomatic_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekBone_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_ear_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_lower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_upper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_levator_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipLevator_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_upper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_lower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekBone_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_zygomatic_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_chin_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_ear_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_slide_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_outer_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_fold_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_fold_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_bulge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_bulge_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_throat_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_elbow_twist_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_arm_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_armBulge_corr_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.R_tassle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassleLoop_01_JNT.R_tassleLoop_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_hood_01_JNT.C_hood_02_JNT.C_hood_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_elbow_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_tassle_01_JNT.L_tassle_02_JNT.L_tassle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_brow_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_corrugator_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_noseBridge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_nostril_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_squint_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_zygomatic_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekBone_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_ear_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_lower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_upper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_levator_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipLevator_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipLevator_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_mentalis_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_depressor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_depressor_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lip_nasolabial_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_mouth_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lip_below_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lip_nasolabial_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_mouth_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_corner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_jaw_clench_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_jaw_clench_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_zygomatic_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_zygomatic_inner_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_cheekUpper_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_upper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_lower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekBone_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_zygomatic_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_chin_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_ear_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_nose_slide_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_brow_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipLevator_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_nostril_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_cheekUpper_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_squint_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_lipUpper_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_lipUpper_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_frontalis_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_outer_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_frontalis_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eyelid_fold_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eyelid_fold_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.L_eye_bulge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.R_eye_bulge_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_elbow_twist_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_tassle_01_JNT.R_tassle_02_JNT.R_tassle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_mentalis_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_depressor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_depressor_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lip_nasolabial_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_mouth_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lip_below_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lip_nasolabial_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_mouth_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_lipLower_corner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_jaw_clench_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_jaw_clench_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.L_zygomatic_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.R_zygomatic_inner_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.L_thumb_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.C_tongue_tip_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.R_thumb_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_thumb_01_JNT.L_thumb_02_JNT.L_thumb_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.L_index_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.L_middle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.L_ring_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.L_pinky_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.C_neck_01_JNT.C_neck_02_JNT.C_head_JNT.C_jaw_JNT.C_tongue_root_JNT.C_tongue_mid_JNT.C_tongue_tip_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_thumb_01_JNT.R_thumb_02_JNT.R_thumb_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.R_index_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.R_middle_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.R_ring_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.R_pinky_03_JNT", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_index_root_JNT.L_index_01_JNT.L_index_02_JNT.L_index_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_middle_root_JNT.L_middle_01_JNT.L_middle_02_JNT.L_middle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_ring_root_JNT.L_ring_01_JNT.L_ring_02_JNT.L_ring_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.L_clavicle_JNT.L_arm_JNT.L_elbow_JNT.L_wrist_JNT.L_pinky_root_JNT.L_pinky_01_JNT.L_pinky_02_JNT.L_pinky_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_index_root_JNT.R_index_01_JNT.R_index_02_JNT.R_index_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_middle_root_JNT.R_middle_01_JNT.R_middle_02_JNT.R_middle_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_ring_root_JNT.R_ring_01_JNT.R_ring_02_JNT.R_ring_03_JNT.transform", + "RootNode.root.C_pelvis_JNT.C_spine_01_JNT.C_spine_02_JNT.C_spine_03_JNT.C_spine_04_JNT.R_clavicle_JNT.R_arm_JNT.R_elbow_JNT.R_wrist_JNT.R_pinky_root_JNT.R_pinky_01_JNT.R_pinky_02_JNT.R_pinky_03_JNT.transform" + ] + }, + "rules": { + "rules": [ + { + "$type": "SkinRule" + }, + { + "$type": "StaticMeshAdvancedRule", + "vertexColorStreamName": "Col0" + }, + { + "$type": "MaterialRule" + } + ] + }, + "id": "{CA754822-3673-46C8-9EE7-3453CF782C5A}" + } + ] +} \ No newline at end of file diff --git a/Gems/MotionMatching/Assets/MotionMatching.animgraph b/Gems/MotionMatching/Assets/MotionMatching.animgraph new file mode 100644 index 0000000000..10707eb6c4 --- /dev/null +++ b/Gems/MotionMatching/Assets/MotionMatching.animgraph @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09cc2374b99c219421812ec39b68a350912d44777a50e3078c8cd67e91cc74cf +size 30927 diff --git a/Gems/MotionMatching/Assets/MotionMatching.emfxworkspace b/Gems/MotionMatching/Assets/MotionMatching.emfxworkspace new file mode 100644 index 0000000000..6d59fa310d --- /dev/null +++ b/Gems/MotionMatching/Assets/MotionMatching.emfxworkspace @@ -0,0 +1,3 @@ +[General] +version=1 +startScript="ImportActor -filename \"Character/RinMM.actor\"\nCreateActorInstance -actorID %LASTRESULT% -xPos 4.585605 -yPos -7.166286 -zPos 0.000000 -xScale 1.000000 -yScale 1.000000 -zScale 1.000000 -rot 0.00000000,0.00000000,0.98711985,-0.15998250\nLoadMotionSet -filename \"@products@/MotionMatching.motionset\"\nLoadAnimGraph -filename \"@products@/MotionMatching.animgraph\"\nActivateAnimGraph -actorInstanceID %LASTRESULT3% -animGraphID %LASTRESULT1% -motionSetID %LASTRESULT2% -visualizeScale 1.000000\n" diff --git a/Gems/MotionMatching/Assets/MotionMatching.motionset b/Gems/MotionMatching/Assets/MotionMatching.motionset new file mode 100644 index 0000000000..276895e3a0 --- /dev/null +++ b/Gems/MotionMatching/Assets/MotionMatching.motionset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c895a1696f5f37492089ceaef11210d704c1fb7e3dd31305cf4732d63489507 +size 13645 diff --git a/Gems/MotionMatching/CMakeLists.txt b/Gems/MotionMatching/CMakeLists.txt new file mode 100644 index 0000000000..341df6e33d --- /dev/null +++ b/Gems/MotionMatching/CMakeLists.txt @@ -0,0 +1,16 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(o3de_gem_path ${CMAKE_CURRENT_LIST_DIR}) +set(o3de_gem_json ${o3de_gem_path}/gem.json) +o3de_read_json_key(o3de_gem_name ${o3de_gem_json} "gem_name") +o3de_restricted_path(${o3de_gem_json} o3de_gem_restricted_path) + +ly_add_external_target_path(${CMAKE_CURRENT_LIST_DIR}/3rdParty) + +add_subdirectory(Code) diff --git a/Gems/MotionMatching/Code/CMakeLists.txt b/Gems/MotionMatching/Code/CMakeLists.txt new file mode 100644 index 0000000000..275f8f2530 --- /dev/null +++ b/Gems/MotionMatching/Code/CMakeLists.txt @@ -0,0 +1,155 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +# Add the MotionMatching.Static target +ly_add_target( + NAME MotionMatching.Static STATIC + NAMESPACE Gem + FILES_CMAKE + motionmatching_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework + Gem::EMotionFXStaticLib + Gem::ImguiAtom.Static +) + +# Here add MotionMatching target, it depends on the MotionMatching.Static +ly_add_target( + NAME MotionMatching ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + motionmatching_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::MotionMatching.Static + Gem::ImGui.Static + Gem::ImGui.ImGuiLYUtils +) + +# By default, we will specify that the above target MotionMatching would be used by +# Client and Server type targets when this gem is enabled. If you don't want it +# active in Clients or Servers by default, delete one of both of the following lines: +ly_create_alias(NAME MotionMatching.Clients NAMESPACE Gem TARGETS Gem::MotionMatching) +ly_create_alias(NAME MotionMatching.Servers NAMESPACE Gem TARGETS Gem::MotionMatching) + +# If we are on a host platform, we want to add the host tools targets like the MotionMatching.Editor target which +# will also depend on MotionMatching.Static +if(PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME MotionMatching.Editor.Static STATIC + NAMESPACE Gem + FILES_CMAKE + motionmatching_editor_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + Gem::MotionMatching.Static + ) + + ly_add_target( + NAME MotionMatching.Editor GEM_MODULE + NAMESPACE Gem + AUTOMOC + OUTPUT_NAME Gem.MotionMatching.Editor + FILES_CMAKE + motionmatching_editor_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + Gem::MotionMatching.Editor.Static + ) + + # By default, we will specify that the above target MotionMatching would be used by + # Tool and Builder type targets when this gem is enabled. If you don't want it + # active in Tools or Builders by default, delete one of both of the following lines: + ly_create_alias(NAME MotionMatching.Tools NAMESPACE Gem TARGETS Gem::MotionMatching.Editor) + ly_create_alias(NAME MotionMatching.Builders NAMESPACE Gem TARGETS Gem::MotionMatching.Editor) + + +endif() + +################################################################################ +# Tests +################################################################################ +# See if globally, tests are supported +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + # We globally support tests, see if we support tests on this platform for MotionMatching.Static + if(PAL_TRAIT_MOTIONMATCHING_TEST_SUPPORTED) + # We support MotionMatching.Tests on this platform, add MotionMatching.Tests target which depends on MotionMatching.Static + ly_add_target( + NAME MotionMatching.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + motionmatching_files.cmake + motionmatching_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzFramework + Gem::EMotionFX.Tests.Static + Gem::MotionMatching.Static + ) + + # Add MotionMatching.Tests to googletest + ly_add_googletest( + NAME Gem::MotionMatching.Tests + ) + endif() + + # If we are a host platform we want to add tools test like editor tests here + if(PAL_TRAIT_BUILD_HOST_TOOLS) + # We are a host platform, see if Editor tests are supported on this platform + if(PAL_TRAIT_MOTIONMATCHING_EDITOR_TEST_SUPPORTED) + # We support MotionMatching.Editor.Tests on this platform, add MotionMatching.Editor.Tests target which depends on MotionMatching.Editor + ly_add_target( + NAME MotionMatching.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + motionmatching_editor_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + Gem::MotionMatching.Editor + ) + + # Add MotionMatching.Editor.Tests to googletest + ly_add_googletest( + NAME Gem::MotionMatching.Editor.Tests + ) + endif() + endif() +endif() diff --git a/Gems/MotionMatching/Code/Include/MotionMatching/MotionMatchingBus.h b/Gems/MotionMatching/Code/Include/MotionMatching/MotionMatchingBus.h new file mode 100644 index 0000000000..5b2bc1847a --- /dev/null +++ b/Gems/MotionMatching/Code/Include/MotionMatching/MotionMatchingBus.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingRequests + { + public: + AZ_RTTI(MotionMatchingRequests, "{b08f73cc-a922-49ef-8c0e-07166b43ea65}"); + virtual ~MotionMatchingRequests() = default; + // Put your public methods here + }; + + class MotionMatchingBusTraits + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + }; + + using MotionMatchingRequestBus = AZ::EBus; + using MotionMatchingInterface = AZ::Interface; + +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/Allocators.h b/Gems/MotionMatching/Code/Source/Allocators.h new file mode 100644 index 0000000000..af6fa27cc8 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/Allocators.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace EMotionFX::MotionMatching +{ + using MotionMatchAllocator = AZ::SystemAllocator; +} // namespace MotionMatching diff --git a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp new file mode 100644 index 0000000000..58cd3903e1 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(BlendTreeMotionMatchNode, AnimGraphAllocator, 0) + AZ_CLASS_ALLOCATOR_IMPL(BlendTreeMotionMatchNode::UniqueData, AnimGraphObjectUniqueDataAllocator, 0) + + BlendTreeMotionMatchNode::BlendTreeMotionMatchNode() + : AnimGraphNode() + { + // Setup the input ports. + InitInputPorts(2); + SetupInputPort("Goal Pos", INPUTPORT_TARGETPOS, MCore::AttributeVector3::TYPE_ID, PORTID_INPUT_TARGETPOS); + SetupInputPort("Goal Facing Dir", INPUTPORT_TARGETFACINGDIR, MCore::AttributeVector3::TYPE_ID, PORTID_INPUT_TARGETFACINGDIR); + + // Setup the output ports. + InitOutputPorts(1); + SetupOutputPortAsPose("Output Pose", OUTPUTPORT_POSE, PORTID_OUTPUT_POSE); + } + + BlendTreeMotionMatchNode::~BlendTreeMotionMatchNode() + { + } + + bool BlendTreeMotionMatchNode::InitAfterLoading(AnimGraph* animGraph) + { + if (!AnimGraphNode::InitAfterLoading(animGraph)) + { + return false; + } + + // Automatically register the default feature schema in case the schema is empty after loading the node. + if (m_featureSchema.GetNumFeatures() == 0) + { + AZStd::string rootJointName; + if (m_animGraph->GetNumAnimGraphInstances() > 0) + { + const Actor* actor = m_animGraph->GetAnimGraphInstance(0)->GetActorInstance()->GetActor(); + const Node* rootJoint = actor->GetMotionExtractionNode(); + if (rootJoint) + { + rootJointName = rootJoint->GetNameString(); + } + } + + DefaultFeatureSchemaInitSettings defaultSettings; + defaultSettings.m_rootJointName = rootJointName.c_str(); + defaultSettings.m_leftFootJointName = "L_foot_JNT"; + defaultSettings.m_rightFootJointName = "R_foot_JNT"; + defaultSettings.m_pelvisJointName = "C_pelvis_JNT"; + DefaultFeatureSchema(m_featureSchema, defaultSettings); + } + + InitInternalAttributesForAllInstances(); + + Reinit(); + return true; + } + + const char* BlendTreeMotionMatchNode::GetPaletteName() const + { + return "Motion Matching"; + } + + AnimGraphObject::ECategory BlendTreeMotionMatchNode::GetPaletteCategory() const + { + return AnimGraphObject::CATEGORY_SOURCES; + } + + void BlendTreeMotionMatchNode::UniqueData::Update() + { + AZ_PROFILE_SCOPE(Animation, "BlendTreeMotionMatchNode::UniqueData::Update"); + + auto animGraphNode = azdynamic_cast(m_object); + AZ_Assert(animGraphNode, "Unique data linked to incorrect node type."); + + ActorInstance* actorInstance = m_animGraphInstance->GetActorInstance(); + + // Clear existing data. + delete m_instance; + delete m_data; + + m_data = aznew MotionMatching::MotionMatchingData(animGraphNode->m_featureSchema); + m_instance = aznew MotionMatching::MotionMatchingInstance(); + + MotionSet* motionSet = m_animGraphInstance->GetMotionSet(); + if (!motionSet) + { + SetHasError(true); + return; + } + + //--------------------------------- + AZ::Debug::Timer timer; + timer.Stamp(); + + // Build a list of motions we want to import the frames from. + AZ_Printf("Motion Matching", "Importing motion database..."); + MotionMatching::MotionMatchingData::InitSettings settings; + settings.m_actorInstance = actorInstance; + settings.m_frameImportSettings.m_sampleRate = animGraphNode->m_sampleRate; + settings.m_importMirrored = animGraphNode->m_mirror; + settings.m_maxKdTreeDepth = animGraphNode->m_maxKdTreeDepth; + settings.m_minFramesPerKdTreeNode = animGraphNode->m_minFramesPerKdTreeNode; + settings.m_motionList.reserve(animGraphNode->m_motionIds.size()); + for (const AZStd::string& id : animGraphNode->m_motionIds) + { + Motion* motion = motionSet->RecursiveFindMotionById(id); + if (motion) + { + settings.m_motionList.emplace_back(motion); + } + else + { + AZ_Warning("Motion Matching", false, "Failed to get motion for motionset entry id '%s'", id.c_str()); + } + } + + // Initialize the motion matching data (slow). + AZ_Printf("Motion Matching", "Initializing motion matching..."); + if (!m_data->Init(settings)) + { + AZ_Warning("Motion Matching", false, "Failed to initialize motion matching for anim graph node '%s'!", animGraphNode->GetName()); + SetHasError(true); + return; + } + + // Initialize the instance. + AZ_Printf("Motion Matching", "Initializing instance..."); + MotionMatching::MotionMatchingInstance::InitSettings initSettings; + initSettings.m_actorInstance = actorInstance; + initSettings.m_data = m_data; + m_instance->Init(initSettings); + + const float initTime = timer.GetDeltaTimeInSeconds(); + const size_t memUsage = m_data->GetFrameDatabase().CalcMemoryUsageInBytes(); + AZ_Printf("Motion Matching", "Finished in %.2f seconds (mem usage=%d bytes or %.2f mb)", initTime, memUsage, memUsage / (float)(1024 * 1024)); + //--------------------------------- + + SetHasError(false); + } + + void BlendTreeMotionMatchNode::Update(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) + { + AZ_PROFILE_SCOPE(Animation, "BlendTreeMotionMatchNode::Update"); + + m_timer.Stamp(); + + UniqueData* uniqueData = static_cast(FindOrCreateUniqueNodeData(animGraphInstance)); + UpdateAllIncomingNodes(animGraphInstance, timePassedInSeconds); + uniqueData->Clear(); + if (uniqueData->GetHasError()) + { + m_updateTimeInMs = 0.0f; + m_postUpdateTimeInMs = 0.0f; + m_outputTimeInMs = 0.0f; + return; + } + + AZ::Vector3 targetPos = AZ::Vector3::CreateZero(); + TryGetInputVector3(animGraphInstance, INPUTPORT_TARGETPOS, targetPos); + + AZ::Vector3 targetFacingDir = AZ::Vector3::CreateAxisY(); + TryGetInputVector3(animGraphInstance, INPUTPORT_TARGETFACINGDIR, targetFacingDir); + + MotionMatching::MotionMatchingInstance* instance = uniqueData->m_instance; + instance->Update(timePassedInSeconds, targetPos, targetFacingDir, m_trajectoryQueryMode, m_pathRadius, m_pathSpeed); + + // set the current time to the new calculated time + uniqueData->ClearInheritFlags(); + uniqueData->SetPreSyncTime(instance->GetMotionInstance()->GetCurrentTime()); + uniqueData->SetCurrentPlayTime(instance->GetNewMotionTime()); + + if (uniqueData->GetPreSyncTime() > uniqueData->GetCurrentPlayTime()) + { + uniqueData->SetPreSyncTime(uniqueData->GetCurrentPlayTime()); + } + + m_updateTimeInMs = m_timer.GetDeltaTimeInSeconds() * 1000.0f; + } + + void BlendTreeMotionMatchNode::PostUpdate(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) + { + AZ_PROFILE_SCOPE(Animation, "BlendTreeMotionMatchNode::PostUpdate"); + + AZ_UNUSED(animGraphInstance); + AZ_UNUSED(timePassedInSeconds); + m_timer.Stamp(); + + for (AZ::u32 i = 0; i < GetNumConnections(); ++i) + { + AnimGraphNode* node = GetConnection(i)->GetSourceNode(); + node->PerformPostUpdate(animGraphInstance, timePassedInSeconds); + } + + UniqueData* uniqueData = static_cast(FindOrCreateUniqueNodeData(animGraphInstance)); + MotionMatching::MotionMatchingInstance* instance = uniqueData->m_instance; + + RequestRefDatas(animGraphInstance); + AnimGraphRefCountedData* data = uniqueData->GetRefCountedData(); + data->ClearEventBuffer(); + data->ZeroTrajectoryDelta(); + + if (uniqueData->GetHasError()) + { + return; + } + + MotionInstance* motionInstance = instance->GetMotionInstance(); + motionInstance->UpdateByTimeValues(uniqueData->GetPreSyncTime(), uniqueData->GetCurrentPlayTime(), &data->GetEventBuffer()); + + uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); + data->GetEventBuffer().UpdateEmitters(this); + + instance->PostUpdate(timePassedInSeconds); + + 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; + } + + void BlendTreeMotionMatchNode::Output(AnimGraphInstance* animGraphInstance) + { + AZ_PROFILE_SCOPE(Animation, "BlendTreeMotionMatchNode::Output"); + + AZ_UNUSED(animGraphInstance); + m_timer.Stamp(); + + AnimGraphPose* outputPose; + + // Initialize to bind pose. + ActorInstance* actorInstance = animGraphInstance->GetActorInstance(); + RequestPoses(animGraphInstance); + outputPose = GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue(); + outputPose->InitFromBindPose(actorInstance); + + if (m_disabled) + { + return; + } + + UniqueData* uniqueData = static_cast(FindOrCreateUniqueNodeData(animGraphInstance)); + if (GetEMotionFX().GetIsInEditorMode()) + { + SetHasError(uniqueData, uniqueData->GetHasError()); + } + + if (uniqueData->GetHasError()) + { + return; + } + + OutputIncomingNode(animGraphInstance, GetInputNode(INPUTPORT_TARGETPOS)); + OutputIncomingNode(animGraphInstance, GetInputNode(INPUTPORT_TARGETFACINGDIR)); + + MotionMatching::MotionMatchingInstance* instance = uniqueData->m_instance; + instance->SetLowestCostSearchFrequency(m_lowestCostSearchFrequency); + + Pose& outTransformPose = outputPose->GetPose(); + instance->Output(outTransformPose); + + // 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); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "Output", m_outputTimeInMs); +#endif + } + + instance->DebugDraw(); + } + + void BlendTreeMotionMatchNode::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(9) + ->Field("sampleRate", &BlendTreeMotionMatchNode::m_sampleRate) + ->Field("lowestCostSearchFrequency", &BlendTreeMotionMatchNode::m_lowestCostSearchFrequency) + ->Field("maxKdTreeDepth", &BlendTreeMotionMatchNode::m_maxKdTreeDepth) + ->Field("minFramesPerKdTreeNode", &BlendTreeMotionMatchNode::m_minFramesPerKdTreeNode) + ->Field("mirror", &BlendTreeMotionMatchNode::m_mirror) + ->Field("controlSplineMode", &BlendTreeMotionMatchNode::m_trajectoryQueryMode) + ->Field("pathRadius", &BlendTreeMotionMatchNode::m_pathRadius) + ->Field("pathSpeed", &BlendTreeMotionMatchNode::m_pathSpeed) + ->Field("featureSchema", &BlendTreeMotionMatchNode::m_featureSchema) + ->Field("motionIds", &BlendTreeMotionMatchNode::m_motionIds) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("Motion Matching Node", "Motion Matching Attributes") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_sampleRate, "Feature sample rate", "The sample rate (in Hz) used for extracting the features from the animations. The higher the sample rate, the more data will be used and the more options the motion matching search has available for the best matching frame.") + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute(AZ::Edit::Attributes::Max, 240) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) + ->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_lowestCostSearchFrequency, "Search frequency", "How often per second we apply the motion matching search and find the lowest cost / best matching frame, and start to blend towards it.") + ->Attribute(AZ::Edit::Attributes::Min, 0.001f) + ->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) + ->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) + ->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) + ->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") + ->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", "") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit) + ->Attribute(AZ::Edit::Attributes::ContainerCanBeModified, false) + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::HideChildren) + ; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h new file mode 100644 index 0000000000..3acae96d2c --- /dev/null +++ b/Gems/MotionMatching/Code/Source/BlendTreeMotionMatchNode.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class EMFX_API BlendTreeMotionMatchNode + : public AnimGraphNode + { + public: + AZ_RTTI(BlendTreeMotionMatchNode, "{1DC80DCD-6536-4950-9260-A4615C03E3C5}", AnimGraphNode) + AZ_CLASS_ALLOCATOR_DECL + + enum + { + INPUTPORT_TARGETPOS = 0, + INPUTPORT_TARGETFACINGDIR = 1, + OUTPUTPORT_POSE = 0 + }; + + enum + { + PORTID_INPUT_TARGETPOS = 0, + PORTID_INPUT_TARGETFACINGDIR = 1, + PORTID_OUTPUT_POSE = 0 + }; + + class EMFX_API UniqueData + : public AnimGraphNodeData + { + EMFX_ANIMGRAPHOBJECTDATA_IMPLEMENT_LOADSAVE + public: + AZ_CLASS_ALLOCATOR_DECL + + UniqueData(AnimGraphNode* node, AnimGraphInstance* animGraphInstance) + : AnimGraphNodeData(node, animGraphInstance) + { + } + + ~UniqueData() + { + delete m_data; + delete m_instance; + } + + void Update() override; + + public: + MotionMatching::MotionMatchingInstance* m_instance = nullptr; + MotionMatching::MotionMatchingData* m_data = nullptr; + }; + + BlendTreeMotionMatchNode(); + ~BlendTreeMotionMatchNode(); + + bool InitAfterLoading(AnimGraph* animGraph) override; + + bool GetSupportsVisualization() const override { return true; } + bool GetHasOutputPose() const override { return true; } + bool GetSupportsDisable() const override { return true; } + AZ::Color GetVisualColor() const override { return AZ::Colors::Green; } + AnimGraphPose* GetMainOutputPose(AnimGraphInstance* animGraphInstance) const override { return GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue(); } + + const char* GetPaletteName() const override; + AnimGraphObject::ECategory GetPaletteCategory() const override; + + AnimGraphObjectData* CreateUniqueData(AnimGraphInstance* animGraphInstance) override { return aznew UniqueData(this, animGraphInstance); } + + static void Reflect(AZ::ReflectContext* context); + + private: + void Output(AnimGraphInstance* animGraphInstance) override; + void Update(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override; + void PostUpdate(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override; + + FeatureSchema m_featureSchema; + AZStd::vector m_motionIds; + + float m_pathRadius = 1.0f; + float m_pathSpeed = 1.0f; + float m_lowestCostSearchFrequency = 5.0f; + AZ::u32 m_sampleRate = 30; + AZ::u32 m_maxKdTreeDepth = 15; + AZ::u32 m_minFramesPerKdTreeNode = 1000; + TrajectoryQuery::EMode m_trajectoryQueryMode = TrajectoryQuery::MODE_TARGETDRIVEN; + bool m_mirror = false; + + AZ::Debug::Timer m_timer; + float m_updateTimeInMs = 0.0f; + float m_postUpdateTimeInMs = 0.0f; + float m_outputTimeInMs = 0.0f; + +#ifdef IMGUI_ENABLED + ImGuiMonitor m_imguiMonitor; +#endif + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/EventData.cpp b/Gems/MotionMatching/Code/Source/EventData.cpp new file mode 100644 index 0000000000..32c05ee58b --- /dev/null +++ b/Gems/MotionMatching/Code/Source/EventData.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(DiscardFrameEventData, MotionEventAllocator, 0) + + bool DiscardFrameEventData::Equal([[maybe_unused]]const EventData& rhs, [[maybe_unused]] bool ignoreEmptyFields) const + { + return true; + } + + void DiscardFrameEventData::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(1) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("[Motion Matching] Discard Frame", "Event used for discarding ranges of the animation..") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ_CRC_CE("Creatable"), true) + ; + } + + /////////////////////////////////////////////////////////////////////////// + + AZ_CLASS_ALLOCATOR_IMPL(TagEventData, MotionEventAllocator, 0) + + bool TagEventData::Equal(const EventData& rhs, [[maybe_unused]] bool ignoreEmptyFields) const + { + const TagEventData* other = azdynamic_cast(&rhs); + if (other) + { + return AZ::StringFunc::Equal(m_tag.c_str(), other->m_tag.c_str(), /*caseSensitive=*/false); + } + return false; + } + + void TagEventData::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(1) + ->Field("tag", &TagEventData::m_tag) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("[Motion Matching] Tag", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ_CRC_CE("Creatable"), true) + ->DataElement(AZ::Edit::UIHandlers::Default, &TagEventData::m_tag, "Tag", "The tag that should be active.") + ; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/EventData.h b/Gems/MotionMatching/Code/Source/EventData.h new file mode 100644 index 0000000000..8b80499b78 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/EventData.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace EMotionFX::MotionMatching +{ + class EMFX_API DiscardFrameEventData + : public EventData + { + public: + AZ_RTTI(DiscardFrameEventData, "{25499823-E611-4958-85B7-476BC1918744}", EventData); + AZ_CLASS_ALLOCATOR_DECL + + DiscardFrameEventData() = default; + ~DiscardFrameEventData() override = default; + + static void Reflect(AZ::ReflectContext* context); + + bool Equal(const EventData& rhs, bool ignoreEmptyFields = false) const override; + + private: + AZStd::string m_tag; + }; + + class EMFX_API TagEventData + : public EventData + { + public: + AZ_RTTI(TagEventData, "{FEFEA2C7-CD68-43B2-94D6-85559E29EABF}", EventData); + AZ_CLASS_ALLOCATOR_DECL + + TagEventData() = default; + ~TagEventData() override = default; + + static void Reflect(AZ::ReflectContext* context); + + bool Equal(const EventData& rhs, bool ignoreEmptyFields = false) const override; + + private: + AZStd::string m_tag; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/Feature.cpp b/Gems/MotionMatching/Code/Source/Feature.cpp new file mode 100644 index 0000000000..0d135d6ed8 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/Feature.cpp @@ -0,0 +1,275 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(Feature, MotionMatchAllocator, 0) + + bool Feature::Init(const InitSettings& settings) + { + const Actor* actor = settings.m_actorInstance->GetActor(); + const Skeleton* skeleton = actor->GetSkeleton(); + + const Node* joint = skeleton->FindNodeByNameNoCase(m_jointName.c_str()); + m_jointIndex = joint ? joint->GetNodeIndex() : InvalidIndex; + if (m_jointIndex == InvalidIndex) + { + AZ_Error("MotionMatching", false, "Feature::Init(): Cannot find index for joint named '%s'.", m_jointName.c_str()); + return false; + } + + const Node* relativeToJoint = skeleton->FindNodeByNameNoCase(m_relativeToJointName.c_str()); + m_relativeToNodeIndex = relativeToJoint ? relativeToJoint->GetNodeIndex() : InvalidIndex; + if (m_relativeToNodeIndex == InvalidIndex) + { + AZ_Error("MotionMatching", false, "Feature::Init(): Cannot find index for joint named '%s'.", m_relativeToJointName.c_str()); + return false; + } + + // Set a default feature name in case it did not get set manually. + if (m_name.empty()) + { + AZStd::string featureTypeName = this->RTTI_GetTypeName(); + AzFramework::StringFunc::Replace(featureTypeName, "Feature", ""); + m_name = AZStd::string::format("%s (%s)", featureTypeName.c_str(), m_jointName.c_str()); + } + return true; + } + + void Feature::SetDebugDrawColor(const AZ::Color& color) + { + m_debugColor = color; + } + + const AZ::Color& Feature::GetDebugDrawColor() const + { + return m_debugColor; + } + + void Feature::SetDebugDrawEnabled(bool enabled) + { + m_debugDrawEnabled = enabled; + } + + bool Feature::GetDebugDrawEnabled() const + { + return m_debugDrawEnabled; + } + + float Feature::CalculateFrameCost([[maybe_unused]] size_t frameIndex, [[maybe_unused]] const FrameCostContext& context) const + { + AZ_Assert(false, "Feature::CalculateFrameCost(): Not implemented for the given feature."); + return 0.0f; + } + + void Feature::SetRelativeToNodeIndex(size_t nodeIndex) + { + m_relativeToNodeIndex = nodeIndex; + } + + void Feature::CalculateVelocity(size_t jointIndex, size_t relativeToJointIndex, MotionInstance* motionInstance, AZ::Vector3& outVelocity) + { + const float originalTime = motionInstance->GetCurrentTime(); + + // Prepare for sampling. + ActorInstance* actorInstance = motionInstance->GetActorInstance(); + AnimGraphPosePool& posePool = GetEMotionFX().GetThreadData(actorInstance->GetThreadIndex())->GetPosePool(); + AnimGraphPose* prevPose = posePool.RequestPose(actorInstance); + AnimGraphPose* currentPose = posePool.RequestPose(actorInstance); + Pose* bindPose = actorInstance->GetTransformData()->GetBindPose(); + + const size_t numSamples = 3; + const float timeRange = 0.05f; // secs + const float halfTimeRange = timeRange * 0.5f; + const float startTime = originalTime - halfTimeRange; + const float frameDelta = timeRange / numSamples; + + AZ::Vector3 accumulatedVelocity = AZ::Vector3::CreateZero(); + + for (size_t sampleIndex = 0; sampleIndex < numSamples + 1; ++sampleIndex) + { + float sampleTime = startTime + sampleIndex * frameDelta; + if (sampleTime < 0.0f) + { + sampleTime = 0.0f; + } + if (sampleTime >= motionInstance->GetMotion()->GetDuration()) + { + sampleTime = motionInstance->GetMotion()->GetDuration(); + } + + if (sampleIndex == 0) + { + motionInstance->SetCurrentTime(sampleTime); + motionInstance->GetMotion()->Update(bindPose, &prevPose->GetPose(), motionInstance); + continue; + } + + motionInstance->SetCurrentTime(sampleTime); + motionInstance->GetMotion()->Update(bindPose, ¤tPose->GetPose(), motionInstance); + + const Transform inverseJointWorldTransform = currentPose->GetPose().GetWorldSpaceTransform(relativeToJointIndex).Inversed(); + + // Calculate the velocity. + const AZ::Vector3 prevPosition = prevPose->GetPose().GetWorldSpaceTransform(jointIndex).m_position; + const AZ::Vector3 currentPosition = currentPose->GetPose().GetWorldSpaceTransform(jointIndex).m_position; + const AZ::Vector3 velocity = CalculateLinearVelocity(prevPosition, currentPosition, frameDelta); + + accumulatedVelocity += inverseJointWorldTransform.TransformVector(velocity); + + *prevPose = *currentPose; + } + + outVelocity = accumulatedVelocity / aznumeric_cast(numSamples); + + motionInstance->SetCurrentTime(originalTime); // set back to what it was + + posePool.FreePose(prevPose); + posePool.FreePose(currentPose); + } + + void Feature::CalculateVelocity(const ActorInstance* actorInstance, size_t jointIndex, size_t relativeToJointIndex, const Frame& frame, AZ::Vector3& outVelocity) + { + AnimGraphPosePool& posePool = GetEMotionFX().GetThreadData(actorInstance->GetThreadIndex())->GetPosePool(); + AnimGraphPose* prevPose = posePool.RequestPose(actorInstance); + AnimGraphPose* currentPose = posePool.RequestPose(actorInstance); + + const size_t numSamples = 3; + const float timeRange = 0.05f; // secs + const float halfTimeRange = timeRange * 0.5f; + const float frameDelta = timeRange / numSamples; + + AZ::Vector3 accumulatedVelocity = AZ::Vector3::CreateZero(); + + for (size_t sampleIndex = 0; sampleIndex < numSamples + 1; ++sampleIndex) + { + const float sampleTimeOffset = (-halfTimeRange) + sampleIndex * frameDelta; + + if (sampleIndex == 0) + { + frame.SamplePose(&prevPose->GetPose(), sampleTimeOffset); + continue; + } + + frame.SamplePose(¤tPose->GetPose(), sampleTimeOffset); + const Transform inverseJointWorldTransform = currentPose->GetPose().GetWorldSpaceTransform(relativeToJointIndex).Inversed(); + + // Calculate the velocity. + const AZ::Vector3 prevPosition = prevPose->GetPose().GetWorldSpaceTransform(jointIndex).m_position; + const AZ::Vector3 currentPosition = currentPose->GetPose().GetWorldSpaceTransform(jointIndex).m_position; + const AZ::Vector3 velocity = CalculateLinearVelocity(prevPosition, currentPosition, frameDelta); + + accumulatedVelocity += inverseJointWorldTransform.TransformVector(velocity); + + *prevPose = *currentPose; + } + + outVelocity = accumulatedVelocity / aznumeric_cast(numSamples); + + posePool.FreePose(prevPose); + posePool.FreePose(currentPose); + } + + float Feature::GetNormalizedDirectionDifference(const AZ::Vector2& directionA, const AZ::Vector2& directionB) const + { + const float dotProduct = directionA.GetNormalized().Dot(directionB.GetNormalized()); + const float normalizedDirectionDifference = (2.0f - (1.0f + dotProduct)) * 0.5f; + return AZ::GetAbs(normalizedDirectionDifference); + } + + float Feature::GetNormalizedDirectionDifference(const AZ::Vector3& directionA, const AZ::Vector3& directionB) const + { + const float dotProduct = directionA.GetNormalized().Dot(directionB.GetNormalized()); + const float normalizedDirectionDifference = (2.0f - (1.0f + dotProduct)) * 0.5f; + return AZ::GetAbs(normalizedDirectionDifference); + } + + float Feature::CalcResidual(float value) const + { + if (m_residualType == ResidualType::Squared) + { + return value * value; + } + + return AZ::Abs(value); + } + + float Feature::CalcResidual(const AZ::Vector3& a, const AZ::Vector3& b) const + { + const float euclideanDistance = (b - a).GetLength(); + return CalcResidual(euclideanDistance); + } + + AZ::Crc32 Feature::GetCostFactorVisibility() const + { + return AZ::Edit::PropertyVisibility::Show; + } + + void Feature::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(2) + ->Field("id", &Feature::m_id) + ->Field("name", &Feature::m_name) + ->Field("jointName", &Feature::m_jointName) + ->Field("relativeToJointName", &Feature::m_relativeToJointName) + ->Field("debugDraw", &Feature::m_debugDrawEnabled) + ->Field("debugColor", &Feature::m_debugColor) + ->Field("costFactor", &Feature::m_costFactor) + ->Field("residualType", &Feature::m_residualType) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("Feature", "Base class for a feature") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ->DataElement(AZ::Edit::UIHandlers::Default, &Feature::m_name, "Name", "Custom name of the feature used for identification and debug visualizations.") + ->DataElement(AZ_CRC_CE("ActorNode"), &Feature::m_jointName, "Joint", "The joint to extract the data from.") + ->DataElement(AZ_CRC_CE("ActorNode"), &Feature::m_relativeToJointName, "Relative To Joint", "When extracting feature data, convert it to relative-space to the given joint.") + ->DataElement(AZ::Edit::UIHandlers::Default, &Feature::m_debugDrawEnabled, "Debug Draw", "Are debug visualizations enabled for this feature?") + ->DataElement(AZ::Edit::UIHandlers::Default, &Feature::m_debugColor, "Debug Draw Color", "Color used for debug visualizations to identify the feature.") + ->DataElement(AZ::Edit::UIHandlers::SpinBox, &Feature::m_costFactor, "Cost Factor", "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.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 100.0f) + ->Attribute(AZ::Edit::Attributes::Step, 0.1f) + ->Attribute(AZ::Edit::Attributes::Visibility, &Feature::GetCostFactorVisibility) + ->DataElement(AZ::Edit::UIHandlers::ComboBox, &Feature::m_residualType, "Residual", "Use 'Squared' in case minimal differences should be ignored and larger differences should overweight others. Use 'Absolute' for linear differences and don't want the mentioned effect.") + ->EnumAttribute(ResidualType::Absolute, "Absolute") + ->EnumAttribute(ResidualType::Squared, "Squared") + ; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/Feature.h b/Gems/MotionMatching/Code/Source/Feature.h new file mode 100644 index 0000000000..9a0fe9fa8c --- /dev/null +++ b/Gems/MotionMatching/Code/Source/Feature.h @@ -0,0 +1,174 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +#include + +namespace EMotionFX +{ + class ActorInstance; + class MotionInstance; + class Pose; + class Motion; +}; + +namespace EMotionFX::MotionMatching +{ + class Frame; + class FrameDatabase; + class MotionMatchingInstance; + class TrajectoryQuery; + + class EMFX_API Feature + { + public: + AZ_RTTI(Feature, "{DE9CBC48-9176-4DF1-8306-4B1E621F0E76}") + AZ_CLASS_ALLOCATOR_DECL + + Feature() = default; + virtual ~Feature() = default; + + //////////////////////////////////////////////////////////////////////// + // Initialization + struct EMFX_API InitSettings + { + ActorInstance* m_actorInstance = nullptr; + FeatureMatrix::Index m_featureColumnStartOffset = 0; + }; + virtual bool Init(const InitSettings& settings); + + //////////////////////////////////////////////////////////////////////// + // Feature extraction + struct EMFX_API ExtractFeatureContext + { + ExtractFeatureContext(FeatureMatrix& featureMatrix) + : m_featureMatrix(featureMatrix) + { + } + + FrameDatabase* m_frameDatabase = nullptr; + FeatureMatrix& m_featureMatrix; + + size_t m_frameIndex = InvalidIndex; + const Pose* m_framePose = nullptr; //! Pre-sampled pose for the given frame. + + ActorInstance* m_actorInstance = nullptr; + }; + virtual void ExtractFeatureValues(const ExtractFeatureContext& context) = 0; + + //////////////////////////////////////////////////////////////////////// + // Feature cost + struct EMFX_API FrameCostContext + { + FrameCostContext(const FeatureMatrix& featureMatrix, const Pose& currentPose) + : m_featureMatrix(featureMatrix) + , m_currentPose(currentPose) + { + } + + const FeatureMatrix& m_featureMatrix; + const ActorInstance* m_actorInstance = nullptr; + const Pose& m_currentPose; //! Current actor instance pose. + const TrajectoryQuery* m_trajectoryQuery; + }; + 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. + enum ResidualType + { + Absolute, + Squared + }; + + void SetCostFactor(float costFactor) { m_costFactor = costFactor; } + float GetCostFactor() const { return m_costFactor; } + + virtual void FillQueryFeatureValues([[maybe_unused]] size_t startIndex, + [[maybe_unused]] AZStd::vector& queryFeatureValues, + [[maybe_unused]] const FrameCostContext& context) {} + + virtual void DebugDraw([[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay, + [[maybe_unused]] MotionMatchingInstance* instance, + [[maybe_unused]] size_t frameIndex) {} + + void SetDebugDrawColor(const AZ::Color& color); + const AZ::Color& GetDebugDrawColor() const; + + void SetDebugDrawEnabled(bool enabled); + bool GetDebugDrawEnabled() const; + + void SetJointName(const AZStd::string& jointName) { m_jointName = jointName; } + const AZStd::string& GetJointName() const { return m_jointName; } + + void SetRelativeToJointName(const AZStd::string& jointName) { m_relativeToJointName = jointName; } + const AZStd::string& GetRelativeToJointName() const { return m_relativeToJointName; } + + void SetName(const AZStd::string& name) { m_name = name; } + const AZStd::string& GetName() const { return m_name; } + + // Column offset for the first value for the given feature inside the feature matrix. + virtual size_t GetNumDimensions() const = 0; + virtual AZStd::string GetDimensionName([[maybe_unused]] size_t index) const { return "Unknown"; } + FeatureMatrix::Index GetColumnOffset() const { return m_featureColumnOffset; } + void SetColumnOffset(FeatureMatrix::Index offset) { m_featureColumnOffset = offset; } + + const AZ::TypeId& GetId() const { return m_id; } + size_t GetRelativeToNodeIndex() const { return m_relativeToNodeIndex; } + void SetRelativeToNodeIndex(size_t nodeIndex); + + static void Reflect(AZ::ReflectContext* context); + static void CalculateVelocity(size_t jointIndex, size_t relativeToJointIndex, MotionInstance* motionInstance, AZ::Vector3& outVelocity); + 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 + **/ + float GetNormalizedDirectionDifference(const AZ::Vector2& directionA, const AZ::Vector2& directionB) const; + float GetNormalizedDirectionDifference(const AZ::Vector3& directionA, const AZ::Vector3& directionB) const; + + float CalcResidual(float value) const; + float CalcResidual(const AZ::Vector3& a, const AZ::Vector3& b) const; + + virtual AZ::Crc32 GetCostFactorVisibility() const; + + // Shared and reflected data. + AZ::TypeId m_id = AZ::TypeId::CreateRandom(); //< The feature identification number. Use this instead of the RTTI class ID so that we can have multiple of the same type. + AZStd::string m_name; //< Display name used for feature identification and debug visualizations. + AZStd::string m_jointName; //< Joint name to extract the data from. + AZStd::string m_relativeToJointName; //< When extracting feature data, convert it to relative-space to the given joint. + 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. + + // 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. + size_t m_relativeToNodeIndex = InvalidIndex; + size_t m_jointIndex = InvalidIndex; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureMatrix.cpp b/Gems/MotionMatching/Code/Source/FeatureMatrix.cpp new file mode 100644 index 0000000000..7da9e146d9 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureMatrix.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FeatureMatrix, MotionMatchAllocator, 0) + + void FeatureMatrix::Clear() + { + resize(0, 0); + } + + void FeatureMatrix::SaveAsCsv(const AZStd::string& filename, const AZStd::vector& columnNames) + { + std::ofstream file(filename.c_str()); + + // Save column names in the first row + if (!columnNames.empty()) + { + for (size_t i = 0; i < columnNames.size(); ++i) + { + if (i != 0) + { + file << ","; + } + + file << columnNames[i].c_str(); + } + file << "\n"; + } + + // Save coefficients +#ifdef O3DE_USE_EIGEN + // Force specify precision, else wise values close to 0.0 get rounded to 0.0. + const static Eigen::IOFormat csvFormat(/*Eigen::StreamPrecision|FullPrecision*/8, Eigen::DontAlignCols, ", ", "\n"); + file << format(csvFormat); +#endif + } + + void FeatureMatrix::SaveAsCsv(const AZStd::string& filename, const FeatureSchema* featureSchema) + { + AZStd::vector columnNames; + + for (Feature* feature: featureSchema->GetFeatures()) + { + const size_t numDimensions = feature->GetNumDimensions(); + for (size_t dimension = 0; dimension < numDimensions; ++dimension) + { + columnNames.push_back(feature->GetDimensionName(dimension)); + } + } + + SaveAsCsv(filename, columnNames); + } + + AZ::Vector2 FeatureMatrix::GetVector2(Index row, Index startColumn) const + { + return AZ::Vector2( + coeff(row, startColumn + 0), + coeff(row, startColumn + 1)); + } + + void FeatureMatrix::SetVector2(Index row, Index startColumn, const AZ::Vector2& value) + { + operator()(row, startColumn + 0) = value.GetX(); + operator()(row, startColumn + 1) = value.GetY(); + } + + AZ::Vector3 FeatureMatrix::GetVector3(Index row, Index startColumn) const + { + return AZ::Vector3( + coeff(row, startColumn + 0), + coeff(row, startColumn + 1), + coeff(row, startColumn + 2)); + } + + void FeatureMatrix::SetVector3(Index row, Index startColumn, const AZ::Vector3& value) + { + operator()(row, startColumn + 0) = value.GetX(); + operator()(row, startColumn + 1) = value.GetY(); + operator()(row, startColumn + 2) = value.GetZ(); + } + + size_t FeatureMatrix::CalcMemoryUsageInBytes() const + { + const size_t bytesPerValue = sizeof(O3DE_MM_FLOATTYPE); + const size_t numValues = size(); + return numValues * bytesPerValue; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureMatrix.h b/Gems/MotionMatching/Code/Source/FeatureMatrix.h new file mode 100644 index 0000000000..1cf42933ce --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureMatrix.h @@ -0,0 +1,118 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +//#define O3DE_USE_EIGEN +#define O3DE_MM_FLOATTYPE float + +#ifdef O3DE_USE_EIGEN +#pragma warning (push, 1) +#pragma warning (disable:4834) // C4834: discarding return value of function with 'nodiscard' attribute +#pragma warning (disable:5031) // #pragma warning(pop): likely mismatch, popping warning state pushed in different file +#pragma warning (disable:4702) // warning C4702: unreachable code +#pragma warning (disable:4723) // warning C4723: potential divide by 0 +#include "../../3rdParty/eigen-3.3.9/Eigen/Dense" +#pragma warning (pop) +#endif + +namespace EMotionFX::MotionMatching +{ + class FeatureSchema; + +#ifdef O3DE_USE_EIGEN + // Features are stored in columns, each row represents a frame + // 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. + */ + class FeatureMatrixType + { + public: + size_t size() const + { + return m_data.size(); + } + + size_t rows() const + { + return m_rowCount; + } + + size_t cols() const + { + return m_columnCount; + } + + void resize(size_t rowCount, size_t columnCount) + { + m_rowCount = rowCount; + m_columnCount = columnCount; + m_data.resize(m_rowCount * m_columnCount); + } + + float& operator()(size_t row, size_t column) + { + return m_data[row * m_columnCount + column]; + } + + const float& operator()(size_t row, size_t column) const + { + return m_data[row * m_columnCount + column]; + } + + float coeff(size_t row, size_t column) const + { + return m_data[row * m_columnCount + column]; + } + + private: + AZStd::vector m_data; + size_t m_rowCount = 0; + size_t m_columnCount = 0; + }; +#endif + + class FeatureMatrix + : public FeatureMatrixType + { + public: + AZ_RTTI(FeatureMatrix, "{E063C9CB-7147-4776-A6E0-98584DD93FEF}"); + AZ_CLASS_ALLOCATOR_DECL + +#ifdef O3DE_USE_EIGEN + using Index = Eigen::Index; +#else + using Index = size_t; +#endif + + virtual ~FeatureMatrix() = default; + + void Clear(); + + void SaveAsCsv(const AZStd::string& filename, const AZStd::vector& columnNames = {}); + void SaveAsCsv(const AZStd::string& filename, const FeatureSchema* featureSchema); + + size_t CalcMemoryUsageInBytes() const; + + AZ::Vector2 GetVector2(Index row, Index startColumn) const; + void SetVector2(Index row, Index startColumn, const AZ::Vector2& value); + + AZ::Vector3 GetVector3(Index row, Index startColumn) const; + void SetVector3(Index row, Index startColumn, const AZ::Vector3& value); + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeaturePosition.cpp b/Gems/MotionMatching/Code/Source/FeaturePosition.cpp new file mode 100644 index 0000000000..b81ec081f7 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeaturePosition.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FeaturePosition, MotionMatchAllocator, 0) + + void FeaturePosition::FillQueryFeatureValues(size_t startIndex, AZStd::vector& queryFeatureValues, const FrameCostContext& context) + { + const Transform invRootTransform = context.m_currentPose.GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + const AZ::Vector3 worldInputPosition = context.m_currentPose.GetWorldSpaceTransform(m_jointIndex).m_position; + const AZ::Vector3 relativeInputPosition = invRootTransform.TransformPoint(worldInputPosition); + queryFeatureValues[startIndex + 0] = relativeInputPosition.GetX(); + queryFeatureValues[startIndex + 1] = relativeInputPosition.GetY(); + queryFeatureValues[startIndex + 2] = relativeInputPosition.GetZ(); + } + + void FeaturePosition::ExtractFeatureValues(const ExtractFeatureContext& context) + { + const Transform invRootTransform = context.m_framePose->GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + const AZ::Vector3 nodeWorldPosition = context.m_framePose->GetWorldSpaceTransform(m_jointIndex).m_position; + const AZ::Vector3 position = invRootTransform.TransformPoint(nodeWorldPosition); + SetFeatureData(context.m_featureMatrix, context.m_frameIndex, position); + } + + void FeaturePosition::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) + { + const MotionMatchingData* data = instance->GetData(); + const ActorInstance* actorInstance = instance->GetActorInstance(); + const Pose* pose = actorInstance->GetTransformData()->GetCurrentPose(); + const Transform jointModelTM = pose->GetModelSpaceTransform(m_jointIndex); + const Transform relativeToWorldTM = pose->GetWorldSpaceTransform(m_relativeToNodeIndex); + + const AZ::Vector3 position = GetFeatureData(data->GetFeatureMatrix(), frameIndex); + const AZ::Vector3 transformedPos = relativeToWorldTM.TransformPoint(position); + + constexpr float markerSize = 0.03f; + debugDisplay.DepthTestOff(); + debugDisplay.SetColor(m_debugColor); + debugDisplay.DrawBall(transformedPos, markerSize, /*drawShaded=*/false); + } + + float FeaturePosition::CalculateFrameCost(size_t frameIndex, const FrameCostContext& context) const + { + const Transform invRootTransform = context.m_currentPose.GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + const AZ::Vector3 worldInputPosition = context.m_currentPose.GetWorldSpaceTransform(m_jointIndex).m_position; + const AZ::Vector3 relativeInputPosition = invRootTransform.TransformPoint(worldInputPosition); + const AZ::Vector3 framePosition = GetFeatureData(context.m_featureMatrix, frameIndex); // This is already relative to the root node + return CalcResidual(relativeInputPosition, framePosition); + } + + void FeaturePosition::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(1); + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("FeaturePosition", "Matches joint positions.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ; + } + + size_t FeaturePosition::GetNumDimensions() const + { + return 3; + } + + AZStd::string FeaturePosition::GetDimensionName(size_t index) const + { + AZStd::string result = m_jointName; + result += '.'; + + switch (index) + { + case 0: { result += "PosX"; break; } + case 1: { result += "PosY"; break; } + case 2: { result += "PosZ"; break; } + default: { result += Feature::GetDimensionName(index); } + } + + return result; + } + + AZ::Vector3 FeaturePosition::GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex) const + { + return featureMatrix.GetVector3(frameIndex, m_featureColumnOffset); + } + + void FeaturePosition::SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, const AZ::Vector3& position) + { + featureMatrix.SetVector3(frameIndex, m_featureColumnOffset, position); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeaturePosition.h b/Gems/MotionMatching/Code/Source/FeaturePosition.h new file mode 100644 index 0000000000..d62dce9a18 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeaturePosition.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace EMotionFX::MotionMatching +{ + class FrameDatabase; + + class EMFX_API FeaturePosition + : public Feature + { + public: + AZ_RTTI(FeaturePosition, "{3EAA6459-DB59-4EA1-B8B3-C933A83AA77D}", Feature) + AZ_CLASS_ALLOCATOR_DECL + + FeaturePosition() = default; + ~FeaturePosition() override = default; + + void ExtractFeatureValues(const ExtractFeatureContext& context) override; + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) override; + + float CalculateFrameCost(size_t frameIndex, const FrameCostContext& context) const override; + + void FillQueryFeatureValues(size_t startIndex, AZStd::vector& queryFeatureValues, const FrameCostContext& context) override; + + static void Reflect(AZ::ReflectContext* context); + + size_t GetNumDimensions() const override; + AZStd::string GetDimensionName(size_t index) const override; + AZ::Vector3 GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex) const; + void SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, const AZ::Vector3& position); + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureSchema.cpp b/Gems/MotionMatching/Code/Source/FeatureSchema.cpp new file mode 100644 index 0000000000..1e36a755ee --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureSchema.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FeatureSchema, MotionMatchAllocator, 0) + + FeatureSchema::~FeatureSchema() + { + Clear(); + } + + Feature* FeatureSchema::GetFeature(size_t index) const + { + return m_features[index]; + } + + const AZStd::vector& FeatureSchema::GetFeatures() const + { + return m_features; + } + + void FeatureSchema::AddFeature(Feature* feature) + { + // Try to see if there is a feature with the same id already. + auto iterator = AZStd::find_if(m_featuresById.begin(), m_featuresById.end(), [&feature](const auto& curEntry) -> bool { + return (feature->GetId() == curEntry.second->GetId()); + }); + + if (iterator != m_featuresById.end()) + { + AZ_Assert(false, "Cannot add feature. Feature with id '%s' has already been registered.", feature->GetId().data); + return; + } + + m_featuresById.emplace(feature->GetId(), feature); + m_features.emplace_back(feature); + } + + void FeatureSchema::Clear() + { + for (Feature* feature : m_features) + { + delete feature; + } + m_featuresById.clear(); + m_features.clear(); + } + + size_t FeatureSchema::GetNumFeatures() const + { + return m_features.size(); + } + + Feature* FeatureSchema::FindFeatureById(const AZ::TypeId& featureId) const + { + const auto result = m_featuresById.find(featureId); + if (result == m_featuresById.end()) + { + return nullptr; + } + + return result->second; + } + + Feature* FeatureSchema::CreateFeatureByType(const AZ::TypeId& typeId) + { + AZ::SerializeContext* context = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationBus::Events::GetSerializeContext); + if (!context) + { + AZ_Error("Motion Matching", false, "Can't get serialize context from component application."); + return nullptr; + } + + const AZ::SerializeContext::ClassData* classData = context->FindClassData(typeId); + if (!classData) + { + AZ_Warning("Motion Matching", false, "Can't find class data for this type."); + return nullptr; + } + + Feature* featureObject = reinterpret_cast(classData->m_factory->Create(classData->m_name)); + return featureObject; + } + + void FeatureSchema::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(1) + ->Field("features", &FeatureSchema::m_features); + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("FeatureSchema", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureSchema::m_features, "Features", "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureSchema.h b/Gems/MotionMatching/Code/Source/FeatureSchema.h new file mode 100644 index 0000000000..d1005ef6dc --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureSchema.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace EMotionFX::MotionMatching +{ + //! The set of features involved in the motion matching search. + //! The schema represents the order of the features as well as their settings while the feature matrix stores the actual feature data. + class EMFX_API FeatureSchema + { + public: + AZ_RTTI(FrameDatabase, "{E34F6BFE-73DB-4DED-AAB9-09FBC5113236}") + AZ_CLASS_ALLOCATOR_DECL + + virtual ~FeatureSchema(); + + void AddFeature(Feature* feature); + void Clear(); + + size_t GetNumFeatures() const; + Feature* GetFeature(size_t index) const; + const AZStd::vector& GetFeatures() const; + + Feature* FindFeatureById(const AZ::TypeId& featureId) const; + + static void Reflect(AZ::ReflectContext* context); + + protected: + static Feature* CreateFeatureByType(const AZ::TypeId& typeId); + + AZStd::vector m_features; //< Ordered set of features (Owns the feature objects). + AZStd::unordered_map m_featuresById; //< Hash-map for fast access to the features by ID. (Weak ownership) + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.cpp b/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.cpp new file mode 100644 index 0000000000..9201c525b6 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + void DefaultFeatureSchema(FeatureSchema& featureSchema, DefaultFeatureSchemaInitSettings settings) + { + featureSchema.Clear(); + const AZStd::string rootJointName = settings.m_rootJointName; + + //---------------------------------------------------------------------------------------------------------- + // Past and future root trajectory + FeatureTrajectory* rootTrajectory = aznew FeatureTrajectory(); + rootTrajectory->SetJointName(rootJointName); + rootTrajectory->SetRelativeToJointName(rootJointName); + rootTrajectory->SetDebugDrawColor(AZ::Color::CreateFromRgba(157,78,221,255)); + rootTrajectory->SetDebugDrawEnabled(true); + featureSchema.AddFeature(rootTrajectory); + + //---------------------------------------------------------------------------------------------------------- + // Left foot position + FeaturePosition* leftFootPosition = aznew FeaturePosition(); + leftFootPosition->SetName("Left Foot Position"); + leftFootPosition->SetJointName(settings.m_leftFootJointName); + leftFootPosition->SetRelativeToJointName(rootJointName); + leftFootPosition->SetDebugDrawColor(AZ::Color::CreateFromRgba(255,173,173,255)); + leftFootPosition->SetDebugDrawEnabled(true); + featureSchema.AddFeature(leftFootPosition); + + //---------------------------------------------------------------------------------------------------------- + // Right foot position + FeaturePosition* rightFootPosition = aznew FeaturePosition(); + rightFootPosition->SetName("Right Foot Position"); + rightFootPosition->SetJointName(settings.m_rightFootJointName); + rightFootPosition->SetRelativeToJointName(rootJointName); + rightFootPosition->SetDebugDrawColor(AZ::Color::CreateFromRgba(253,255,182,255)); + rightFootPosition->SetDebugDrawEnabled(true); + featureSchema.AddFeature(rightFootPosition); + + //---------------------------------------------------------------------------------------------------------- + // Left foot velocity + FeatureVelocity* leftFootVelocity = aznew FeatureVelocity(); + leftFootVelocity->SetName("Left Foot Velocity"); + leftFootVelocity->SetJointName(settings.m_leftFootJointName); + leftFootVelocity->SetRelativeToJointName(rootJointName); + leftFootVelocity->SetDebugDrawColor(AZ::Color::CreateFromRgba(155,246,255,255)); + leftFootVelocity->SetDebugDrawEnabled(true); + leftFootVelocity->SetCostFactor(0.75f); + featureSchema.AddFeature(leftFootVelocity); + + //---------------------------------------------------------------------------------------------------------- + // Right foot velocity + FeatureVelocity* rightFootVelocity = aznew FeatureVelocity(); + rightFootVelocity->SetName("Right Foot Velocity"); + rightFootVelocity->SetJointName(settings.m_rightFootJointName); + rightFootVelocity->SetRelativeToJointName(rootJointName); + rightFootVelocity->SetDebugDrawColor(AZ::Color::CreateFromRgba(189,178,255,255)); + rightFootVelocity->SetDebugDrawEnabled(true); + rightFootVelocity->SetCostFactor(0.75f); + featureSchema.AddFeature(rightFootVelocity); + + //---------------------------------------------------------------------------------------------------------- + // Pelvis velocity + FeatureVelocity* pelvisVelocity = aznew FeatureVelocity(); + pelvisVelocity->SetName("Pelvis Velocity"); + pelvisVelocity->SetJointName(settings.m_pelvisJointName); + pelvisVelocity->SetRelativeToJointName(rootJointName); + pelvisVelocity->SetDebugDrawColor(AZ::Color::CreateFromRgba(185,255,175,255)); + pelvisVelocity->SetDebugDrawEnabled(true); + featureSchema.AddFeature(pelvisVelocity); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.h b/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.h new file mode 100644 index 0000000000..0c9cda228f --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureSchemaDefault.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace EMotionFX::MotionMatching +{ + struct DefaultFeatureSchemaInitSettings + { + AZStd::string m_rootJointName; + AZStd::string m_leftFootJointName; + AZStd::string m_rightFootJointName; + AZStd::string m_pelvisJointName; + }; + void DefaultFeatureSchema(FeatureSchema& featureSchema, DefaultFeatureSchemaInitSettings settings); +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureTrajectory.cpp b/Gems/MotionMatching/Code/Source/FeatureTrajectory.cpp new file mode 100644 index 0000000000..3de6053b06 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureTrajectory.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FeatureTrajectory, MotionMatchAllocator, 0) + + bool FeatureTrajectory::Init(const InitSettings& settings) + { + const bool result = Feature::Init(settings); + UpdateFacingAxis(); + return result; + } + + size_t FeatureTrajectory::CalcNumSamplesPerFrame() const + { + return m_numPastSamples + 1 + m_numFutureSamples; + } + + void FeatureTrajectory::SetFacingAxis(const Axis axis) + { + m_facingAxis = axis; + UpdateFacingAxis(); + } + + void FeatureTrajectory::UpdateFacingAxis() + { + switch (m_facingAxis) + { + case Axis::X: + { + m_facingAxisDir = AZ::Vector3::CreateAxisX(); + break; + } + case Axis::Y: + { + m_facingAxisDir = AZ::Vector3::CreateAxisY(); + break; + } + case Axis::X_NEGATIVE: + { + m_facingAxisDir = -AZ::Vector3::CreateAxisX(); + break; + } + case Axis::Y_NEGATIVE: + { + m_facingAxisDir = -AZ::Vector3::CreateAxisY(); + break; + } + default: + { + AZ_Assert(false, "Facing direction axis unknown."); + } + } + } + + AZ::Vector2 FeatureTrajectory::CalculateFacingDirection(const Pose& pose, const Transform& invRootTransform) const + { + // Get the facing direction of the given joint for the given pose in animation world space. + // The given pose is either sampled into the relative past or future based on the frame we want to extract the feature for. + const AZ::Vector3 facingDirAnimationWorldSpace = pose.GetWorldSpaceTransform(m_jointIndex).TransformVector(m_facingAxisDir); + + // The invRootTransform is the inverse of the world space transform for the given joint at the frame we want to extract the feature for. + // The result after this will be the facing direction relative to the frame we want to extract the feature for. + const AZ::Vector3 facingDirection = invRootTransform.TransformVector(facingDirAnimationWorldSpace); + + // Project to the ground plane and make sure the direction is normalized. + return AZ::Vector2(facingDirection).GetNormalizedSafe(); + } + + FeatureTrajectory::Sample FeatureTrajectory::GetSampleFromPose(const Pose& pose, const Transform& invRootTransform) const + { + // Position of the root joint in the model space relative to frame to extract. + const AZ::Vector2 position = AZ::Vector2(invRootTransform.TransformPoint(pose.GetWorldSpaceTransform(m_jointIndex).m_position)); + + // Calculate the facing direction. + const AZ::Vector2 facingDirection = CalculateFacingDirection(pose, invRootTransform); + + return { position, facingDirection }; + } + + void FeatureTrajectory::ExtractFeatureValues(const ExtractFeatureContext& context) + { + const ActorInstance* actorInstance = context.m_actorInstance; + AnimGraphPosePool& posePool = GetEMotionFX().GetThreadData(actorInstance->GetThreadIndex())->GetPosePool(); + AnimGraphPose* samplePose = posePool.RequestPose(actorInstance); + AnimGraphPose* nextSamplePose = posePool.RequestPose(actorInstance); + + const size_t frameIndex = context.m_frameIndex; + const Frame& currentFrame = context.m_frameDatabase->GetFrame(context.m_frameIndex); + + // Inverse of the root transform for the frame that we want to extract data from. + const Transform invRootTransform = context.m_framePose->GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + + const size_t midSampleIndex = CalcMidFrameIndex(); + const Sample midSample = GetSampleFromPose(*context.m_framePose, invRootTransform); + SetFeatureData(context.m_featureMatrix, frameIndex, midSampleIndex, midSample); + + // Sample the past. + const float pastFrameTimeDelta = m_pastTimeRange / static_cast(m_numPastSamples - 1); + currentFrame.SamplePose(&samplePose->GetPose()); + for (size_t i = 0; i < m_numPastSamples; ++i) + { + // Increase the sample index by one as the zeroth past/future sample actually needs one time delta time difference to the current frame. + const float sampleTimeOffset = (i+1) * pastFrameTimeDelta * (-1.0f); + currentFrame.SamplePose(&nextSamplePose->GetPose(), sampleTimeOffset); + + const Sample sample = GetSampleFromPose(samplePose->GetPose(), invRootTransform); + const size_t sampleIndex = CalcPastFrameIndex(i); + SetFeatureData(context.m_featureMatrix, frameIndex, sampleIndex, sample); + + *samplePose = *nextSamplePose; + } + + // Sample into the future. + const float futureFrameTimeDelta = m_futureTimeRange / (float)(m_numFutureSamples - 1); + currentFrame.SamplePose(&samplePose->GetPose()); + for (size_t i = 0; i < m_numFutureSamples; ++i) + { + // Sample the value at the future sample point. + const float sampleTimeOffset = (i+1) * futureFrameTimeDelta; + currentFrame.SamplePose(&nextSamplePose->GetPose(), sampleTimeOffset); + + const Sample sample = GetSampleFromPose(samplePose->GetPose(), invRootTransform); + const size_t sampleIndex = CalcFutureFrameIndex(i); + SetFeatureData(context.m_featureMatrix, frameIndex, sampleIndex, sample); + + *samplePose = *nextSamplePose; + } + + posePool.FreePose(samplePose); + posePool.FreePose(nextSamplePose); + } + + void FeatureTrajectory::SetPastTimeRange(float timeInSeconds) + { + m_pastTimeRange = timeInSeconds; + } + + void FeatureTrajectory::SetFutureTimeRange(float timeInSeconds) + { + m_futureTimeRange = timeInSeconds; + } + + void FeatureTrajectory::SetNumPastSamplesPerFrame(size_t numHistorySamples) + { + m_numPastSamples = numHistorySamples; + } + + void FeatureTrajectory::SetNumFutureSamplesPerFrame(size_t numFutureSamples) + { + m_numFutureSamples = numFutureSamples; + } + + void FeatureTrajectory::DebugDrawFacingDirection(AzFramework::DebugDisplayRequests& debugDisplay, + const AZ::Vector3& positionWorldSpace, + const AZ::Vector3& facingDirectionWorldSpace) + { + const float length = 0.2f; + const float radius = 0.01f; + + const AZ::Vector3 facingDirectionTarget = positionWorldSpace + facingDirectionWorldSpace * length; + debugDisplay.DrawSolidCylinder(/*center=*/(facingDirectionTarget + positionWorldSpace) * 0.5f, + /*direction=*/facingDirectionWorldSpace, + radius, + /*height=*/length, + /*drawShaded=*/false); + } + + void FeatureTrajectory::DebugDrawFacingDirection(AzFramework::DebugDisplayRequests& debugDisplay, + const Transform& worldSpaceTransform, + const Sample& sample, + const AZ::Vector3& samplePosWorldSpace) const + { + const AZ::Vector3 facingDirectionWorldSpace = worldSpaceTransform.TransformVector(AZ::Vector3(sample.m_facingDirection)).GetNormalizedSafe(); + DebugDrawFacingDirection(debugDisplay, samplePosWorldSpace, facingDirectionWorldSpace); + } + + void FeatureTrajectory::DebugDrawTrajectory(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex, + const Transform& worldSpaceTransform, + const AZ::Color& color, + size_t numSamples, + const SplineToFeatureMatrixIndex& splineToFeatureMatrixIndex) const + { + if (frameIndex == InvalidIndex) + { + return; + } + + constexpr float markerSize = 0.02f; + const FeatureMatrix& featureMatrix = instance->GetData()->GetFeatureMatrix(); + + debugDisplay.DepthTestOff(); + debugDisplay.SetColor(color); + + Sample nextSample; + AZ::Vector3 nextSamplePos; + for (size_t i = 0; i < numSamples - 1; ++i) + { + const Sample currentSample = GetFeatureData(featureMatrix, frameIndex, splineToFeatureMatrixIndex(i)); + nextSample = GetFeatureData(featureMatrix, frameIndex, splineToFeatureMatrixIndex(i + 1)); + + const AZ::Vector3 currentSamplePos = worldSpaceTransform.TransformPoint(AZ::Vector3(currentSample.m_position)); + nextSamplePos = worldSpaceTransform.TransformPoint(AZ::Vector3(nextSample.m_position)); + + // Line between current and next sample. + debugDisplay.DrawSolidCylinder(/*center=*/(nextSamplePos + currentSamplePos) * 0.5f, + /*direction=*/(nextSamplePos - currentSamplePos).GetNormalizedSafe(), + /*radius=*/0.0025f, + /*height=*/(nextSamplePos - currentSamplePos).GetLength(), + /*drawShaded=*/false); + + // Sphere at the sample position and a cylinder to indicate the facing direction. + debugDisplay.DrawBall(currentSamplePos, markerSize, /*drawShaded=*/false); + DebugDrawFacingDirection(debugDisplay, worldSpaceTransform, currentSample, currentSamplePos); + } + + debugDisplay.DrawBall(nextSamplePos, markerSize, /*drawShaded=*/false); + DebugDrawFacingDirection(debugDisplay, worldSpaceTransform, nextSample, nextSamplePos); + } + + void FeatureTrajectory::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) + { + const ActorInstance* actorInstance = instance->GetActorInstance(); + const Transform transform = actorInstance->GetTransformData()->GetCurrentPose()->GetWorldSpaceTransform(m_jointIndex); + + DebugDrawTrajectory(debugDisplay, instance, frameIndex, transform, + m_debugColor, m_numPastSamples, AZStd::bind(&FeatureTrajectory::CalcPastFrameIndex, this, AZStd::placeholders::_1)); + + DebugDrawTrajectory(debugDisplay, instance, frameIndex, transform, + m_debugColor, m_numFutureSamples, AZStd::bind(&FeatureTrajectory::CalcFutureFrameIndex, this, AZStd::placeholders::_1)); + } + + size_t FeatureTrajectory::CalcMidFrameIndex() const + { + return m_numPastSamples; + } + + size_t FeatureTrajectory::CalcPastFrameIndex(size_t historyFrameIndex) const + { + AZ_Assert(historyFrameIndex < m_numPastSamples, "The history frame index is out of range"); + return m_numPastSamples - historyFrameIndex - 1; + } + + size_t FeatureTrajectory::CalcFutureFrameIndex(size_t futureFrameIndex) const + { + AZ_Assert(futureFrameIndex < m_numFutureSamples, "The future frame index is out of range"); + return CalcMidFrameIndex() + 1 + futureFrameIndex; + } + + float FeatureTrajectory::CalculateCost(const FeatureMatrix& featureMatrix, + size_t frameIndex, + const Transform& invRootTransform, + const AZStd::vector& controlPoints, + const SplineToFeatureMatrixIndex& splineToFeatureMatrixIndex) const + { + float cost = 0.0f; + AZ::Vector2 lastControlPoint, lastSamplePos; + + for (size_t i = 0; i < controlPoints.size(); ++i) + { + const TrajectoryQuery::ControlPoint& controlPoint = controlPoints[i]; + const Sample sample = GetFeatureData(featureMatrix, frameIndex, splineToFeatureMatrixIndex(i)); + const AZ::Vector2& samplePos = sample.m_position; + const AZ::Vector2 controlPointPos = AZ::Vector2(invRootTransform.TransformPoint(controlPoint.m_position)); // Convert so it is relative to where we are and pointing to. + + if (i != 0) + { + const AZ::Vector2 controlPointDelta = controlPointPos - lastControlPoint; + const AZ::Vector2 sampleDelta = samplePos - lastSamplePos; + + const float posDistance = (samplePos - controlPointPos).GetLength(); + const float posDeltaDistance = (controlPointDelta - sampleDelta).GetLength(); + + // The facing direction from the control point (trajectory query) is in world space while the facing direction from the + // sample of this trajectory feature is in relative-to-frame-root-joint space. + const AZ::Vector2 controlPointFacingDirRelativeSpace = AZ::Vector2(invRootTransform.TransformVector(controlPoint.m_facingDirection)); + const float facingDirectionCost = GetNormalizedDirectionDifference(sample.m_facingDirection, + controlPointFacingDirRelativeSpace); + + // As we got two different costs for the position, double the cost of the facing direction to equal out the influence. + cost += CalcResidual(posDistance) + CalcResidual(posDeltaDistance) + CalcResidual(facingDirectionCost) * 2.0f; + } + + lastControlPoint = controlPointPos; + lastSamplePos = samplePos; + } + + return cost; + } + + float FeatureTrajectory::CalculateFutureFrameCost(size_t frameIndex, const FrameCostContext& context) const + { + AZ_Assert(context.m_trajectoryQuery->GetFutureControlPoints().size() == m_numFutureSamples, "Number of future control points from the trajectory query does not match the one from the trajectory feature."); + const Transform invRootTransform = context.m_currentPose.GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + return CalculateCost(context.m_featureMatrix, frameIndex, invRootTransform, context.m_trajectoryQuery->GetFutureControlPoints(), AZStd::bind(&FeatureTrajectory::CalcFutureFrameIndex, this, AZStd::placeholders::_1)); + } + + float FeatureTrajectory::CalculatePastFrameCost(size_t frameIndex, const FrameCostContext& context) const + { + AZ_Assert(context.m_trajectoryQuery->GetPastControlPoints().size() == m_numPastSamples, "Number of past control points from the trajectory query does not match the one from the trajectory feature"); + const Transform invRootTransform = context.m_currentPose.GetWorldSpaceTransform(m_relativeToNodeIndex).Inversed(); + return CalculateCost(context.m_featureMatrix, frameIndex, invRootTransform, context.m_trajectoryQuery->GetPastControlPoints(), AZStd::bind(&FeatureTrajectory::CalcPastFrameIndex, this, AZStd::placeholders::_1)); + } + + AZ::Crc32 FeatureTrajectory::GetCostFactorVisibility() const + { + return AZ::Edit::PropertyVisibility::Hide; + } + + void FeatureTrajectory::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(2) + ->Field("pastTimeRange", &FeatureTrajectory::m_pastTimeRange) + ->Field("numPastSamples", &FeatureTrajectory::m_numPastSamples) + ->Field("pastCostFactor", &FeatureTrajectory::m_pastCostFactor) + ->Field("futureTimeRange", &FeatureTrajectory::m_futureTimeRange) + ->Field("numFutureSamples", &FeatureTrajectory::m_numFutureSamples) + ->Field("futureCostFactor", &FeatureTrajectory::m_futureCostFactor) + ->Field("facingAxis", &FeatureTrajectory::m_facingAxis) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("FeatureTrajectory", "Matches the joint past and future trajectory.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_numPastSamples, "Past Samples", "The number of samples stored per frame for the past trajectory. [Default = 4 samples to represent the trajectory history]") + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute(AZ::Edit::Attributes::Max, 100) + ->Attribute(AZ::Edit::Attributes::Step, 1) + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_pastTimeRange, "Past Time Range", "The time window the samples are distributed along for the trajectory history. [Default = 0.7 seconds]") + ->Attribute(AZ::Edit::Attributes::Min, 0.01f) + ->Attribute(AZ::Edit::Attributes::Max, 10.0f) + ->Attribute(AZ::Edit::Attributes::Step, 0.1f) + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_pastCostFactor, "Past Cost Factor", "The cost factor is multiplied with the cost from the trajectory history and can be used to change the influence of the trajectory history match in the motion matching search.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 100.0f) + ->Attribute(AZ::Edit::Attributes::Step, 0.1f) + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_numFutureSamples, "Future Samples", "The number of samples stored per frame for the future trajectory. [Default = 6 samples to represent the future trajectory]") + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute(AZ::Edit::Attributes::Max, 100) + ->Attribute(AZ::Edit::Attributes::Step, 1) + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_futureTimeRange, "Future Time Range", "The time window the samples are distributed along for the future trajectory. [Default = 1.2 seconds]") + ->Attribute(AZ::Edit::Attributes::Min, 0.01f) + ->Attribute(AZ::Edit::Attributes::Max, 10.0f) + ->Attribute(AZ::Edit::Attributes::Step, 0.1f) + ->DataElement(AZ::Edit::UIHandlers::Default, &FeatureTrajectory::m_futureCostFactor, "Future Cost Factor", "The cost factor is multiplied with the cost from the future trajectory and can be used to change the influence of the future trajectory match in the motion matching search.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 100.0f) + ->Attribute(AZ::Edit::Attributes::Step, 0.1f) + ->DataElement(AZ::Edit::UIHandlers::ComboBox, &FeatureTrajectory::m_facingAxis, "Facing Axis", "The facing direction of the character. Which axis of the joint transform is facing forward? [Default = Looking into Y-axis direction]") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FeatureTrajectory::UpdateFacingAxis) + ->EnumAttribute(Axis::X, "X") + ->EnumAttribute(Axis::X_NEGATIVE, "-X") + ->EnumAttribute(Axis::Y, "Y") + ->EnumAttribute(Axis::Y_NEGATIVE, "-Y") + ; + } + + size_t FeatureTrajectory::GetNumDimensions() const + { + return CalcNumSamplesPerFrame() * Sample::s_componentsPerSample; + } + + AZStd::string FeatureTrajectory::GetDimensionName(size_t index) const + { + AZStd::string result = "Trajectory"; + + const int sampleIndex = aznumeric_cast(index) / aznumeric_cast(Sample::s_componentsPerSample); + const int componentIndex = index % Sample::s_componentsPerSample; + const int midSampleIndex = aznumeric_cast(CalcMidFrameIndex()); + + if (sampleIndex == midSampleIndex) + { + result += ".Current."; + } + else if (sampleIndex < midSampleIndex) + { + result += AZStd::string::format(".Past%i.", sampleIndex - static_cast(m_numPastSamples)); + } + else + { + result += AZStd::string::format(".Future%i.", sampleIndex - static_cast(m_numPastSamples)); + } + + switch (componentIndex) + { + case 0: { result += "PosX"; break; } + case 1: { result += "PosY"; break; } + case 2: { result += "FacingDirX"; break; } + case 3: { result += "FacingDirY"; break; } + default: { result += Feature::GetDimensionName(index); } + } + + return result; + } + + FeatureTrajectory::Sample FeatureTrajectory::GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex, size_t sampleIndex) const + { + const size_t columnOffset = m_featureColumnOffset + sampleIndex * Sample::s_componentsPerSample; + return { + /*.m_position =*/ featureMatrix.GetVector2(frameIndex, columnOffset + 0), + /*.m_facingDirection =*/ featureMatrix.GetVector2(frameIndex, columnOffset + 2), + }; + } + + void FeatureTrajectory::SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, size_t sampleIndex, const Sample& sample) + { + const size_t columnOffset = m_featureColumnOffset + sampleIndex * Sample::s_componentsPerSample; + featureMatrix.SetVector2(frameIndex, columnOffset + 0, sample.m_position); + featureMatrix.SetVector2(frameIndex, columnOffset + 2, sample.m_facingDirection); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureTrajectory.h b/Gems/MotionMatching/Code/Source/FeatureTrajectory.h new file mode 100644 index 0000000000..7eacbf684c --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureTrajectory.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +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. + **/ + class EMFX_API FeatureTrajectory + : public Feature + { + public: + AZ_RTTI(FeatureTrajectory, "{0451E95B-A452-439A-81ED-3962A06A3992}", Feature) + AZ_CLASS_ALLOCATOR_DECL + + enum class Axis + { + X = 0, + Y = 1, + X_NEGATIVE = 2, + Y_NEGATIVE = 3, + }; + + struct EMFX_API Sample + { + AZ::Vector2 m_position; //! Position in the space relative to the extracted frame. + AZ::Vector2 m_facingDirection; //! Facing direction in the space relative to the extracted frame. + + static constexpr size_t s_componentsPerSample = 4; + }; + + FeatureTrajectory() = default; + ~FeatureTrajectory() override = default; + + bool Init(const InitSettings& settings) override; + void ExtractFeatureValues(const ExtractFeatureContext& context) override; + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) override; + + float CalculateFutureFrameCost(size_t frameIndex, const FrameCostContext& context) const; + float CalculatePastFrameCost(size_t frameIndex, const FrameCostContext& context) const; + + void SetNumPastSamplesPerFrame(size_t numHistorySamples); + void SetNumFutureSamplesPerFrame(size_t numFutureSamples); + void SetPastTimeRange(float timeInSeconds); + void SetFutureTimeRange(float timeInSeconds); + void SetFacingAxis(const Axis axis); + void UpdateFacingAxis(); + + float GetPastTimeRange() const { return m_pastTimeRange; } + size_t GetNumPastSamples() const { return m_numPastSamples; } + float GetPastCostFactor() const { return m_pastCostFactor; } + + float GetFutureTimeRange() const { return m_futureTimeRange; } + size_t GetNumFutureSamples() const { return m_numFutureSamples; } + float GetFutureCostFactor() const { return m_futureCostFactor; } + + AZ::Vector2 CalculateFacingDirection(const Pose& pose, const Transform& invRootTransform) const; + AZ::Vector3 GetFacingAxisDir() const { return m_facingAxisDir; } + + static void Reflect(AZ::ReflectContext* context); + + size_t GetNumDimensions() const override; + AZStd::string GetDimensionName(size_t index) const override; + + // Shared helper function to draw a facing direction. + static void DebugDrawFacingDirection(AzFramework::DebugDisplayRequests& debugDisplay, + const AZ::Vector3& positionWorldSpace, + const AZ::Vector3& facingDirectionWorldSpace); + + private: + size_t CalcMidFrameIndex() const; + size_t CalcPastFrameIndex(size_t historyFrameIndex) const; + size_t CalcFutureFrameIndex(size_t futureFrameIndex) const; + size_t CalcNumSamplesPerFrame() const; + + using SplineToFeatureMatrixIndex = AZStd::function; + float CalculateCost(const FeatureMatrix& featureMatrix, + size_t frameIndex, + const Transform& invRootTransform, + const AZStd::vector& controlPoints, + const SplineToFeatureMatrixIndex& splineToFeatureMatrixIndex) const; + + //! Called for every sample in the past or future range to extract its information. + //! @param[in] pose The sampled pose within the trajectory range [m_pastTimeRange, m_futureTimeRange]. + //! @param[in] invRootTransform The inverse of the world space transform of the joint at frame time that the feature is extracted for. + Sample GetSampleFromPose(const Pose& pose, const Transform& invRootTransform) const; + + Sample GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex, size_t sampleIndex) const; + void SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, size_t sampleIndex, const Sample& sample); + + void DebugDrawTrajectory(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex, + const Transform& transform, + const AZ::Color& color, + size_t numSamples, + const SplineToFeatureMatrixIndex& splineToFeatureMatrixIndex) const; + + void DebugDrawFacingDirection(AzFramework::DebugDisplayRequests& debugDisplay, + const Transform& worldSpaceTransform, + const Sample& sample, + const AZ::Vector3& samplePosWorldSpace) const; + + AZ::Crc32 GetCostFactorVisibility() const override; + + float m_pastTimeRange = 0.7f; //< The time window the samples are distributed along for the past trajectory. + size_t m_numPastSamples = 4; //< The number of samples stored per frame for the past (history) trajectory. + float m_pastCostFactor = 0.5f; //< Normalized value to weight or scale the future trajectory cost. + + float m_futureTimeRange = 1.2f; //< The time window the samples are distributed along for the future trajectory. + size_t m_numFutureSamples = 6; //< The number of samples stored per frame for the future trajectory. + float m_futureCostFactor = 0.75f; //< Normalized value to weight or scale the future trajectory cost. + + Axis m_facingAxis = Axis::Y; //< Which axis of the joint transform is facing forward? + AZ::Vector3 m_facingAxisDir = AZ::Vector3::CreateAxisY(); + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureVelocity.cpp b/Gems/MotionMatching/Code/Source/FeatureVelocity.cpp new file mode 100644 index 0000000000..e210ca1a61 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureVelocity.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FeatureVelocity, MotionMatchAllocator, 0) + + void FeatureVelocity::FillQueryFeatureValues(size_t startIndex, AZStd::vector& queryFeatureValues, const FrameCostContext& context) + { + PoseDataJointVelocities* velocityPoseData = static_cast(context.m_currentPose.GetPoseDataByType(azrtti_typeid())); + AZ_Assert(velocityPoseData, "Cannot calculate velocity feature cost without joint velocity pose data."); + const AZ::Vector3 currentVelocity = velocityPoseData->GetVelocity(m_jointIndex); + + queryFeatureValues[startIndex + 0] = currentVelocity.GetX(); + queryFeatureValues[startIndex + 1] = currentVelocity.GetY(); + queryFeatureValues[startIndex + 2] = currentVelocity.GetZ(); + } + + void FeatureVelocity::ExtractFeatureValues(const ExtractFeatureContext& context) + { + AZ::Vector3 velocity; + CalculateVelocity(context.m_actorInstance, m_jointIndex, m_relativeToNodeIndex, context.m_frameDatabase->GetFrame(context.m_frameIndex), velocity); + + SetFeatureData(context.m_featureMatrix, context.m_frameIndex, velocity); + } + + void FeatureVelocity::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + const AZ::Vector3& velocity, + size_t jointIndex, + size_t relativeToJointIndex, + const AZ::Color& color) + { + const ActorInstance* actorInstance = instance->GetActorInstance(); + const Pose* pose = actorInstance->GetTransformData()->GetCurrentPose(); + const Transform jointModelTM = pose->GetModelSpaceTransform(jointIndex); + const Transform relativeToWorldTM = pose->GetWorldSpaceTransform(relativeToJointIndex); + + const AZ::Vector3 jointPosition = relativeToWorldTM.TransformPoint(jointModelTM.m_position); + const float scale = 0.15f; + const AZ::Vector3 velocityWorldSpace = relativeToWorldTM.TransformVector(velocity * scale); + + DebugDrawVelocity(debugDisplay, jointPosition, velocityWorldSpace, color); + } + + void FeatureVelocity::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) + { + if (m_jointIndex == InvalidIndex) + { + return; + } + + const MotionMatchingData* data = instance->GetData(); + const AZ::Vector3 velocity = GetFeatureData(data->GetFeatureMatrix(), frameIndex); + DebugDraw(debugDisplay, instance, velocity, m_jointIndex, m_relativeToNodeIndex, m_debugColor); + } + + float FeatureVelocity::CalculateFrameCost(size_t frameIndex, const FrameCostContext& context) const + { + PoseDataJointVelocities* velocityPoseData = static_cast(context.m_currentPose.GetPoseDataByType(azrtti_typeid())); + AZ_Assert(velocityPoseData, "Cannot calculate velocity feature cost without joint velocity pose data."); + const AZ::Vector3 currentVelocity = velocityPoseData->GetVelocity(m_jointIndex); + + const AZ::Vector3 frameVelocity = GetFeatureData(context.m_featureMatrix, frameIndex); + + // Direction difference + const float directionDifferenceCost = GetNormalizedDirectionDifference(frameVelocity.GetNormalized(), currentVelocity.GetNormalized()); + + // Speed difference + // TODO: This needs to be normalized later on, else wise it could be that the direction difference is weights + // too heavily or too less compared to what the speed values are + const float speedDifferenceCost = frameVelocity.GetLength() - currentVelocity.GetLength(); + + return CalcResidual(directionDifferenceCost) + CalcResidual(speedDifferenceCost); + } + + void FeatureVelocity::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (!serializeContext) + { + return; + } + + serializeContext->Class() + ->Version(1) + ; + + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (!editContext) + { + return; + } + + editContext->Class("FeatureVelocity", "Matches joint velocities.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, "") + ; + } + + size_t FeatureVelocity::GetNumDimensions() const + { + return 3; + } + + AZStd::string FeatureVelocity::GetDimensionName(size_t index) const + { + AZStd::string result = m_jointName; + result += '.'; + + switch (index) + { + case 0: { result += "VelocityX"; break; } + case 1: { result += "VelocityY"; break; } + case 2: { result += "VelocityZ"; break; } + default: { result += Feature::GetDimensionName(index); } + } + + return result; + } + + AZ::Vector3 FeatureVelocity::GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex) const + { + return featureMatrix.GetVector3(frameIndex, m_featureColumnOffset); + } + + void FeatureVelocity::SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, const AZ::Vector3& velocity) + { + featureMatrix.SetVector3(frameIndex, m_featureColumnOffset, velocity); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FeatureVelocity.h b/Gems/MotionMatching/Code/Source/FeatureVelocity.h new file mode 100644 index 0000000000..37cd8f3d7e --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FeatureVelocity.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace EMotionFX::MotionMatching +{ + class FrameDatabase; + + class EMFX_API FeatureVelocity + : public Feature + { + public: + AZ_RTTI(FeatureVelocity, "{DEEA4F0F-CE70-4F16-9136-C2BFDDA29336}", Feature) + AZ_CLASS_ALLOCATOR_DECL + + FeatureVelocity() = default; + ~FeatureVelocity() override = default; + + void ExtractFeatureValues(const ExtractFeatureContext& context) override; + + static void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + const AZ::Vector3& velocity, // in world space + size_t jointIndex, + size_t relativeToJointIndex, + const AZ::Color& color); + + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, + MotionMatchingInstance* instance, + size_t frameIndex) override; + + float CalculateFrameCost(size_t frameIndex, const FrameCostContext& context) const override; + + void FillQueryFeatureValues(size_t startIndex, AZStd::vector& queryFeatureValues, const FrameCostContext& context) override; + + static void Reflect(AZ::ReflectContext* context); + + size_t GetNumDimensions() const override; + AZStd::string GetDimensionName(size_t index) const override; + AZ::Vector3 GetFeatureData(const FeatureMatrix& featureMatrix, size_t frameIndex) const; + void SetFeatureData(FeatureMatrix& featureMatrix, size_t frameIndex, const AZ::Vector3& velocity); + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/Frame.cpp b/Gems/MotionMatching/Code/Source/Frame.cpp new file mode 100644 index 0000000000..170d3050f7 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/Frame.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(Frame, MotionMatchAllocator, 0) + + Frame::Frame() + : m_frameIndex(InvalidIndex) + , m_sampleTime(0.0f) + , m_sourceMotion(nullptr) + , m_mirrored(false) + { + } + + Frame::Frame(size_t frameIndex, Motion* sourceMotion, float sampleTime, bool mirrored) + : m_frameIndex(frameIndex) + , m_sourceMotion(sourceMotion) + , m_sampleTime(sampleTime) + , m_mirrored(mirrored) + { + } + + void Frame::SamplePose(Pose* outputPose, float timeOffset) const + { + MotionDataSampleSettings sampleSettings; + sampleSettings.m_actorInstance = outputPose->GetActorInstance(); + sampleSettings.m_inPlace = false; + sampleSettings.m_mirror = m_mirrored; + sampleSettings.m_retarget = false; + sampleSettings.m_inputPose = sampleSettings.m_actorInstance->GetTransformData()->GetBindPose(); + + sampleSettings.m_sampleTime = m_sampleTime + timeOffset; + sampleSettings.m_sampleTime = AZ::GetClamp(m_sampleTime + timeOffset, 0.0f, m_sourceMotion->GetDuration()); + + m_sourceMotion->SamplePose(outputPose, sampleSettings); + } + + void Frame::SetFrameIndex(size_t frameIndex) + { + m_frameIndex = frameIndex; + } + + Motion* Frame::GetSourceMotion() const + { + return m_sourceMotion; + } + + float Frame::GetSampleTime() const + { + return m_sampleTime; + } + + void Frame::SetSourceMotion(Motion* sourceMotion) + { + m_sourceMotion = sourceMotion; + } + + void Frame::SetMirrored(bool enabled) + { + m_mirrored = enabled; + } + + void Frame::SetSampleTime(float sampleTime) + { + m_sampleTime = sampleTime; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/Frame.h b/Gems/MotionMatching/Code/Source/Frame.h new file mode 100644 index 0000000000..02150cfa5a --- /dev/null +++ b/Gems/MotionMatching/Code/Source/Frame.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include +#include + +namespace EMotionFX +{ + class Motion; + + namespace MotionMatching + { + /** + * A motion matching frame. + * This holds information required in order to extract a given pose in a given motion. + */ + class EMFX_API Frame + { + public: + AZ_RTTI(Frame, "{985BD732-D80E-4898-AB6C-CAB22D88AACD}") + AZ_CLASS_ALLOCATOR_DECL + + Frame(); + Frame(size_t frameIndex, Motion* sourceMotion, float sampleTime, bool mirrored); + ~Frame() = default; + + //! Sample the pose for the given frame. + //! @param[in] outputPose The pose used to store the sampled result. + //! @param[in] timeOffset Frames in the frame database are samples with a given sample rate (default = 30 fps). + //! For calculating velocities for example, it is needed to sample a pose close to a frame but not exactly at the frame position. + //! The timeOffset parameter can be used for that and represents the offset in time from the frame sample time in seconds. + //! In case the time offset is 0.0, the pose exactly at the frame position will be sampled. + void SamplePose(Pose* outputPose, float timeOffset = 0.0f) const; + + Motion* GetSourceMotion() const; + float GetSampleTime() const; + size_t GetFrameIndex() const { return m_frameIndex; } + bool GetMirrored() const { return m_mirrored; } + + void SetSourceMotion(Motion* sourceMotion); + void SetSampleTime(float sampleTime); + void SetFrameIndex(size_t frameIndex); + 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? */ + }; + } // namespace MotionMatching +} // namespace EMotionFX diff --git a/Gems/MotionMatching/Code/Source/FrameDatabase.cpp b/Gems/MotionMatching/Code/Source/FrameDatabase.cpp new file mode 100644 index 0000000000..7d38060dcc --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FrameDatabase.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(FrameDatabase, MotionMatchAllocator, 0) + + FrameDatabase::FrameDatabase() + { + } + + FrameDatabase::~FrameDatabase() + { + Clear(); + } + + void FrameDatabase::Clear() + { + // Clear the frames. + m_frames.clear(); + m_frames.shrink_to_fit(); + + m_frameIndexByMotion.clear(); + + // Clear other things. + m_usedMotions.clear(); + m_usedMotions.shrink_to_fit(); + } + + void FrameDatabase::ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector& activeEventDatas) + { + activeEventDatas.clear(); + + // Iterate over all motion event tracks and all events inside them. + const MotionEventTable* eventTable = motion->GetEventTable(); + const size_t numTracks = eventTable->GetNumTracks(); + for (size_t t = 0; t < numTracks; ++t) + { + const MotionEventTrack* track = eventTable->GetTrack(t); + const size_t numEvents = track->GetNumEvents(); + for (size_t e = 0; e < numEvents; ++e) + { + const MotionEvent& motionEvent = track->GetEvent(e); + + // Only handle range based events and events that include our time value. + if (motionEvent.GetIsTickEvent() || + motionEvent.GetStartTime() > time || + motionEvent.GetEndTime() < time) + { + continue; + } + + for (auto eventData : motionEvent.GetEventDatas()) + { + activeEventDatas.emplace_back(const_cast(eventData.get())); + } + } + } + } + + bool FrameDatabase::IsFrameDiscarded(const AZStd::vector& activeEventDatas) const + { + for (const EventData* eventData : activeEventDatas) + { + if (eventData->RTTI_GetType() == azrtti_typeid()) + { + return true; + } + } + + return false; + } + + AZStd::tuple FrameDatabase::ImportFrames(Motion* motion, const FrameImportSettings& settings, bool mirrored) + { + AZ_PROFILE_SCOPE(Animation, "FrameDatabase::ImportFrames"); + + AZ_Assert(motion, "The motion cannot be a nullptr"); + AZ_Assert(settings.m_sampleRate > 0, "The sample rate must be bigger than zero frames per second"); + AZ_Assert(settings.m_sampleRate <= 120, "The sample rate must be smaller than 120 frames per second"); + + size_t numFramesImported = 0; + size_t numFramesDiscarded = 0; + + // Calculate the number of frames we might need to import, in worst case. + m_sampleRate = settings.m_sampleRate; + const double timeStep = 1.0 / aznumeric_cast(settings.m_sampleRate); + const size_t worstCaseNumFrames = aznumeric_cast(ceil(motion->GetDuration() / timeStep)) + 1; + + // Try to pre-allocate memory for the worst case scenario. + if (m_frames.capacity() < m_frames.size() + worstCaseNumFrames) + { + m_frames.reserve(m_frames.size() + worstCaseNumFrames); + } + + AZStd::vector activeEvents; + + // Iterate over all sample positions in the motion. + const double totalTime = aznumeric_cast(motion->GetDuration()); + double curTime = 0.0; + while (curTime <= totalTime) + { + const float floatTime = aznumeric_cast(curTime); + ExtractActiveMotionEventDatas(motion, floatTime, activeEvents); + if (!IsFrameDiscarded(activeEvents)) + { + ImportFrame(motion, floatTime, mirrored); + numFramesImported++; + } + else + { + numFramesDiscarded++; + } + curTime += timeStep; + } + + // 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)) + { + ImportFrame(motion, floatTime, mirrored); + numFramesImported++; + } + else + { + numFramesDiscarded++; + } + } + + // Automatically shrink the frame storage to their minimum size. + if (settings.m_autoShrink) + { + m_frames.shrink_to_fit(); + } + + // Register the motion. + if (AZStd::find(m_usedMotions.begin(), m_usedMotions.end(), motion) == m_usedMotions.end()) + { + m_usedMotions.emplace_back(motion); + } + + return { numFramesImported, numFramesDiscarded }; + } + + void FrameDatabase::ImportFrame(Motion* motion, float timeValue, bool mirrored) + { + m_frames.emplace_back(Frame(m_frames.size(), motion, timeValue, mirrored)); + m_frameIndexByMotion[motion].emplace_back(m_frames.back().GetFrameIndex()); + } + + size_t FrameDatabase::CalcMemoryUsageInBytes() const + { + size_t total = 0; + + total += m_frames.capacity() * sizeof(Frame); + total += sizeof(m_frames); + total += m_usedMotions.capacity() * sizeof(const Motion*); + total += sizeof(m_usedMotions); + + return total; + } + + size_t FrameDatabase::GetNumFrames() const + { + return m_frames.size(); + } + + size_t FrameDatabase::GetNumUsedMotions() const + { + return m_usedMotions.size(); + } + + const Motion* FrameDatabase::GetUsedMotion(size_t index) const + { + return m_usedMotions[index]; + } + + const Frame& FrameDatabase::GetFrame(size_t index) const + { + AZ_Assert(index < m_frames.size(), "Frame index is out of range!"); + return m_frames[index]; + } + + AZStd::vector& FrameDatabase::GetFrames() + { + return m_frames; + } + + const AZStd::vector& FrameDatabase::GetFrames() const + { + return m_frames; + } + + const AZStd::vector& FrameDatabase::GetUsedMotions() const + { + return m_usedMotions; + } + + size_t FrameDatabase::FindFrameIndex(Motion* motion, float playtime) const + { + auto iterator = m_frameIndexByMotion.find(motion); + if (iterator == m_frameIndexByMotion.end()) + { + return InvalidIndex; + } + + const AZStd::vector& frameIndices = iterator->second; + for (const size_t frameIndex : frameIndices) + { + const Frame& frame = m_frames[frameIndex]; + if (playtime >= frame.GetSampleTime() && + frameIndex + 1 < m_frames.size() && + playtime <= m_frames[frameIndex + 1].GetSampleTime()) + { + return frameIndex; + } + } + + return InvalidIndex; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/FrameDatabase.h b/Gems/MotionMatching/Code/Source/FrameDatabase.h new file mode 100644 index 0000000000..c5258e1b39 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/FrameDatabase.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace EMotionFX +{ + class Motion; + class ActorInstance; +} + +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. + 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(). + 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. */ + }; + + 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. + + // Statistics. + size_t GetNumFrames() const; + size_t GetNumUsedMotions() const; + size_t CalcMemoryUsageInBytes() const; + + // Misc. + const Motion* GetUsedMotion(size_t index) const; + const Frame& GetFrame(size_t index) const; + const AZStd::vector& GetFrames() const; + AZStd::vector& GetFrames(); + 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. + */ + 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. + + 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::unordered_map> m_frameIndexByMotion; + AZStd::vector m_usedMotions; /**< The list of used motions. */ + size_t m_sampleRate = 0; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/ImGuiMonitor.cpp b/Gems/MotionMatching/Code/Source/ImGuiMonitor.cpp new file mode 100644 index 0000000000..97b666c693 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/ImGuiMonitor.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#ifdef IMGUI_ENABLED +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(ImGuiMonitor, MotionMatchAllocator, 0) + + ImGuiMonitor::ImGuiMonitor() + { + m_performanceStats.m_name = "Performance Statistics"; + + m_featureCosts.m_name = "Feature Costs"; + m_featureCosts.m_histogramContainerCount = 100; + + ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); + ImGuiMonitorRequestBus::Handler::BusConnect(); + } + + ImGuiMonitor::~ImGuiMonitor() + { + ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect(); + ImGuiMonitorRequestBus::Handler::BusDisconnect(); + } + + void ImGuiMonitor::OnImGuiUpdate() + { + if (!m_performanceStats.m_show && !m_featureCosts.m_show) + { + return; + } + + if (ImGui::Begin("Motion Matching")) + { + if (ImGui::CollapsingHeader("Feature Matrix", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) + { + ImGui::Text("Memory Usage: %.2f MB", m_featureMatrixMemoryUsageInBytes / 1024.0f / 1024.0f); + ImGui::Text("Num Frames: %zu", m_featureMatrixNumFrames); + ImGui::Text("Num Feature Components: %zu", m_featureMatrixNumComponents); + } + + if (ImGui::CollapsingHeader("Kd-Tree", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) + { + ImGui::Text("Memory Usage: %.2f MB", m_kdTreeMemoryUsageInBytes / 1024.0f / 1024.0f); + ImGui::Text("Num Nodes: %zu", m_kdTreeNumNodes); + ImGui::Text("Num Dimensions: %zu", m_kdTreeNumDimensions); + } + + m_performanceStats.OnImGuiUpdate(); + m_featureCosts.OnImGuiUpdate(); + } + } + + void ImGuiMonitor::OnImGuiMainMenuUpdate() + { + if (ImGui::BeginMenu("Motion Matching")) + { + ImGui::MenuItem(m_performanceStats.m_name.c_str(), "", &m_performanceStats.m_show); + ImGui::MenuItem(m_featureCosts.m_name.c_str(), "", &m_featureCosts.m_show); + ImGui::EndMenu(); + } + } + + void ImGuiMonitor::PushPerformanceHistogramValue(const char* performanceMetricName, float value) + { + m_performanceStats.PushHistogramValue(performanceMetricName, value, AZ::Color::CreateFromRgba(229,56,59,255)); + } + + void ImGuiMonitor::PushCostHistogramValue(const char* costName, float value, const AZ::Color& color) + { + m_featureCosts.PushHistogramValue(costName, value, color); + } + + void ImGuiMonitor::HistogramGroup::PushHistogramValue(const char* valueName, float value, const AZ::Color& color) + { + auto iterator = m_histogramIndexByName.find(valueName); + if (iterator != m_histogramIndexByName.end()) + { + ImGui::LYImGuiUtils::HistogramContainer& histogramContiner = m_histograms[iterator->second]; + histogramContiner.PushValue(value); + histogramContiner.SetBarLineColor(ImColor(color.GetR(), color.GetG(), color.GetB(), color.GetA())); + } + else + { + ImGui::LYImGuiUtils::HistogramContainer newHistogram; + newHistogram.Init(/*histogramName=*/valueName, + /*containerCount=*/m_histogramContainerCount, + /*viewType=*/ImGui::LYImGuiUtils::HistogramContainer::ViewType::Histogram, + /*displayOverlays=*/true, + /*min=*/0.0f, + /*max=*/0.0f); + + newHistogram.SetMoveDirection(ImGui::LYImGuiUtils::HistogramContainer::PushRightMoveLeft); + newHistogram.PushValue(value); + + m_histogramIndexByName[valueName] = m_histograms.size(); + m_histograms.push_back(newHistogram); + } + } + + void ImGuiMonitor::HistogramGroup::OnImGuiUpdate() + { + if (!m_show) + { + return; + } + + if (ImGui::CollapsingHeader(m_name.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) + { + for (auto& histogram : m_histograms) + { + ImGui::BeginGroup(); + { + histogram.Draw(ImGui::GetColumnWidth() - 70, s_histogramHeight); + + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0,0,0,255)); + { + const ImColor color = histogram.GetBarLineColor(); + ImGui::PushStyleColor(ImGuiCol_Button, color.Value); + { + const AZStd::string valueString = AZStd::string::format("%.2f", histogram.GetLastValue()); + ImGui::Button(valueString.c_str()); + } + ImGui::PopStyleColor(); + } + ImGui::PopStyleColor(); + } + ImGui::EndGroup(); + } + } + } +} // namespace EMotionFX::MotionMatching + +#endif // IMGUI_ENABLED diff --git a/Gems/MotionMatching/Code/Source/ImGuiMonitor.h b/Gems/MotionMatching/Code/Source/ImGuiMonitor.h new file mode 100644 index 0000000000..0583d0ba41 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/ImGuiMonitor.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once +#ifdef IMGUI_ENABLED + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class EMFX_API ImGuiMonitor + : public ImGui::ImGuiUpdateListenerBus::Handler + , public ImGuiMonitorRequestBus::Handler + { + public: + AZ_RTTI(ImGuiMonitor, "{BF1B85A4-215C-4E3A-8FD8-CE3233E5C779}") + AZ_CLASS_ALLOCATOR_DECL + + ImGuiMonitor(); + ~ImGuiMonitor(); + + // ImGui::ImGuiUpdateListenerBus::Handler + void OnImGuiUpdate() override; + void OnImGuiMainMenuUpdate() override; + + // ImGuiMonitorRequestBus::Handler + void PushPerformanceHistogramValue(const char* performanceMetricName, float value) override; + void PushCostHistogramValue(const char* costName, float value, const AZ::Color& color) override; + + void SetFeatureMatrixMemoryUsage(size_t sizeInBytes) override { m_featureMatrixMemoryUsageInBytes = sizeInBytes; } + void SetFeatureMatrixNumFrames(size_t numFrames) override { m_featureMatrixNumFrames = numFrames; } + void SetFeatureMatrixNumComponents(size_t numFeatureComponents) override { m_featureMatrixNumComponents = numFeatureComponents; } + + void SetKdTreeMemoryUsage(size_t sizeInBytes) override { m_kdTreeMemoryUsageInBytes = sizeInBytes; } + void SetKdTreeNumNodes(size_t numNodes) override { m_kdTreeNumNodes = numNodes; } + void SetKdTreeNumDimensions(size_t numDimensions) override { m_kdTreeNumDimensions = numDimensions; } + + private: + //! Named and sub-divided group containing several histograms. + struct HistogramGroup + { + void OnImGuiUpdate(); + void PushHistogramValue(const char* valueName, float value, const AZ::Color& color); + + bool m_show = true; + AZStd::string m_name; + using HistogramIndexByNames = AZStd::unordered_map; + HistogramIndexByNames m_histogramIndexByName; + AZStd::vector m_histograms; + int m_histogramContainerCount = 500; + + static constexpr float s_histogramHeight = 95.0f; + }; + + HistogramGroup m_performanceStats; + HistogramGroup m_featureCosts; + + size_t m_featureMatrixMemoryUsageInBytes = 0; + size_t m_featureMatrixNumFrames = 0; + size_t m_featureMatrixNumComponents = 0; + + size_t m_kdTreeMemoryUsageInBytes = 0; + size_t m_kdTreeNumNodes = 0; + size_t m_kdTreeNumDimensions = 0; + }; +} // namespace EMotionFX::MotionMatching + +#endif // IMGUI_ENABLED diff --git a/Gems/MotionMatching/Code/Source/ImGuiMonitorBus.h b/Gems/MotionMatching/Code/Source/ImGuiMonitorBus.h new file mode 100644 index 0000000000..7c50b317c5 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/ImGuiMonitorBus.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class ImGuiMonitorRequests + : public AZ::EBusTraits + { + public: + // Enable multi-threaded access by locking primitive using a mutex when connecting handlers to the EBus or executing events. + using MutexType = AZStd::recursive_mutex; + + virtual void PushPerformanceHistogramValue(const char* performanceMetricName, float value) = 0; + virtual void PushCostHistogramValue(const char* costName, float value, const AZ::Color& color) = 0; + + virtual void SetFeatureMatrixMemoryUsage(size_t sizeInBytes) = 0; + virtual void SetFeatureMatrixNumFrames(size_t numFrames) = 0; + virtual void SetFeatureMatrixNumComponents(size_t numFeatureComponents) = 0; + + virtual void SetKdTreeMemoryUsage(size_t sizeInBytes) = 0; + virtual void SetKdTreeNumNodes(size_t numNodes) = 0; + virtual void SetKdTreeNumDimensions(size_t numDimensions) = 0; + }; + using ImGuiMonitorRequestBus = AZ::EBus; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/KdTree.cpp b/Gems/MotionMatching/Code/Source/KdTree.cpp new file mode 100644 index 0000000000..e496f0b63e --- /dev/null +++ b/Gems/MotionMatching/Code/Source/KdTree.cpp @@ -0,0 +1,454 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(KdTree, MotionMatchAllocator, 0) + + KdTree::~KdTree() + { + Clear(); + } + + size_t KdTree::CalcNumDimensions(const AZStd::vector& features) + { + size_t result = 0; + for (Feature* feature : features) + { + if (feature->GetId().IsNull()) + { + continue; + } + + result += feature->GetNumDimensions(); + } + return result; + } + + bool KdTree::Init(const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + size_t maxDepth, + size_t minFramesPerLeaf) + { + AZ::Debug::Timer timer; + timer.Stamp(); + + 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) + { + 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); + return false; + } + + if (minFramesPerLeaf > 100000) + { + AZ_Error("Motion Matching", false, "KdTree minFramesPerLeaf (%d) cannot be smaller than 100000.", minFramesPerLeaf); + return false; + } + + if (maxDepth == 0) + { + AZ_Error("Motion Matching", false, "KdTree max depth (%d) cannot be zero", maxDepth); + return false; + } + + m_maxDepth = maxDepth; + m_minFramesPerLeaf = minFramesPerLeaf; + + // Build the tree. + m_featureValues.resize(m_numDimensions); + BuildTreeNodes(frameDatabase, featureMatrix, features, new Node(), nullptr, 0); + MergeSmallLeafNodesToParents(); + ClearFramesForNonEssentialNodes(); + RemoveZeroFrameLeafNodes(); + + const float initTime = timer.GetDeltaTimeInSeconds(); + AZ_TracePrintf("EMotionFX", "KdTree initialized in %f seconds (numNodes = %d numDims = %d Memory used = %.2f MB).", + initTime, m_nodes.size(), + m_numDimensions, + static_cast(CalcMemoryUsageInBytes()) / 1024.0f / 1024.0f); + + PrintStats(); + return true; + } + + void KdTree::Clear() + { + // delete all nodes + for (Node* node : m_nodes) + { + delete node; + } + + m_nodes.clear(); + m_featureValues.clear(); + m_numDimensions = 0; + } + + size_t KdTree::CalcMemoryUsageInBytes() const + { + size_t totalBytes = 0; + + for (const Node* node : m_nodes) + { + totalBytes += sizeof(Node); + totalBytes += node->m_frames.capacity() * sizeof(size_t); + } + + totalBytes += m_featureValues.capacity() * sizeof(float); + totalBytes += sizeof(KdTree); + return totalBytes; + } + + bool KdTree::IsInitialized() const + { + return (m_numDimensions != 0); + } + + size_t KdTree::GetNumNodes() const + { + return m_nodes.size(); + } + + size_t KdTree::GetNumDimensions() const + { + return m_numDimensions; + } + + void KdTree::BuildTreeNodes(const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + Node* node, + Node* parent, + size_t dimension, + bool leftSide) + { + node->m_parent = parent; + node->m_dimension = dimension; + m_nodes.emplace_back(node); + + // Fill the frames array and calculate the median. + FillFramesForNode(node, frameDatabase, featureMatrix, features, parent, leftSide); + + // Prevent splitting further when we don't want to. + const size_t maxDimensions = AZ::GetMin(m_numDimensions, m_maxDepth); + if (node->m_frames.size() < m_minFramesPerLeaf * 2 || + dimension >= maxDimensions) + { + return; + } + + // Create the left node. + Node* leftNode = new Node(); + AZ_Assert(!node->m_leftNode, "Expected the parent left node to be a nullptr"); + node->m_leftNode = leftNode; + BuildTreeNodes(frameDatabase, featureMatrix, features, leftNode, node, dimension + 1, true); + + // Create the right node. + Node* rightNode = new Node(); + AZ_Assert(!node->m_rightNode, "Expected the parent right node to be a nullptr"); + node->m_rightNode = rightNode; + BuildTreeNodes(frameDatabase, featureMatrix, features, rightNode, node, dimension + 1, false); + } + + void KdTree::ClearFramesForNonEssentialNodes() + { + for (Node* node : m_nodes) + { + if (node->m_leftNode && node->m_rightNode) + { + node->m_frames.clear(); + node->m_frames.shrink_to_fit(); + } + } + } + + void KdTree::RemoveLeafNode(Node* node) + { + Node* parent = node->m_parent; + + if (parent->m_leftNode == node) + { + parent->m_leftNode = nullptr; + } + + if (parent->m_rightNode == node) + { + parent->m_rightNode = nullptr; + } + + // Remove it from the node vector. + const auto location = AZStd::find(m_nodes.begin(), m_nodes.end(), node); + AZ_Assert(location != m_nodes.end(), "Expected to find the item to remove."); + m_nodes.erase(location); + + delete node; + } + + void KdTree::MergeSmallLeafNodesToParents() + { + AZStd::vector nodesToRemove; + for (Node* node : m_nodes) + { + // If we are a leaf node and we don't have enough frames. + if ((!node->m_leftNode && !node->m_rightNode) && + node->m_frames.size() < m_minFramesPerLeaf) + { + nodesToRemove.emplace_back(node); + } + } + + // Remove the actual nodes. + for (Node* node : nodesToRemove) + { + RemoveLeafNode(node); + } + } + + void KdTree::RemoveZeroFrameLeafNodes() + { + AZStd::vector nodesToRemove; + + // Build a list of leaf nodes to remove. + // These are ones that have no feature inside them. + for (Node* node : m_nodes) + { + if ((!node->m_leftNode && !node->m_rightNode) && + node->m_frames.empty()) + { + nodesToRemove.emplace_back(node); + } + } + + // Remove the actual nodes. + for (Node* node : nodesToRemove) + { + RemoveLeafNode(node); + } + } + + void KdTree::FillFramesForNode(Node* node, + const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + Node* parent, + bool leftSide) + { + float median = 0.0f; + if (parent) + { + // Assume half of the parent frames are in this node. + node->m_frames.reserve((parent->m_frames.size() / 2) + 1); + + // Add parent frames to this node, but only ones that should be on this side. + for (const size_t frameIndex : parent->m_frames) + { + FillFeatureValues(featureMatrix, features, frameIndex); + + const float value = m_featureValues[parent->m_dimension]; + if (leftSide) + { + if (value <= parent->m_median) + { + node->m_frames.emplace_back(frameIndex); + } + } + else + { + if (value > parent->m_median) + { + node->m_frames.emplace_back(frameIndex); + } + } + + median += value; + } + } + else // We're the root node. + { + node->m_frames.reserve(frameDatabase.GetNumFrames()); + for (const Frame& frame : frameDatabase.GetFrames()) + { + const size_t frameIndex = frame.GetFrameIndex(); + node->m_frames.emplace_back(frameIndex); + FillFeatureValues(featureMatrix, features, frameIndex); + median += m_featureValues[node->m_dimension]; + } + } + + if (!node->m_frames.empty()) + { + median /= static_cast(node->m_frames.size()); + } + node->m_median = median; + } + + void KdTree::FillFeatureValues(const FeatureMatrix& featureMatrix, const Feature* feature, size_t frameIndex, size_t startIndex) + { + const size_t numDimensions = feature->GetNumDimensions(); + const size_t featureColumnOffset = feature->GetColumnOffset(); + for (size_t i = 0; i < numDimensions; ++i) + { + m_featureValues[startIndex + i] = featureMatrix(frameIndex, featureColumnOffset + i); + } + } + + void KdTree::FillFeatureValues(const FeatureMatrix& featureMatrix, const AZStd::vector& features, size_t frameIndex) + { + size_t startDimension = 0; + for (const Feature* feature : features) + { + FillFeatureValues(featureMatrix, feature, frameIndex, startDimension); + startDimension += feature->GetNumDimensions(); + } + } + + void KdTree::RecursiveCalcNumFrames(Node* node, size_t& outNumFrames) const + { + if (node->m_leftNode && node->m_rightNode) + { + RecursiveCalcNumFrames(node->m_leftNode, outNumFrames); + RecursiveCalcNumFrames(node->m_rightNode, outNumFrames); + } + else + { + outNumFrames += node->m_frames.size(); + } + } + + void KdTree::PrintStats() + { + size_t leftNumFrames = 0; + size_t rightNumFrames = 0; + if (m_nodes[0]->m_leftNode) + { + RecursiveCalcNumFrames(m_nodes[0]->m_leftNode, leftNumFrames); + } + + if (m_nodes[0]->m_rightNode) + { + RecursiveCalcNumFrames(m_nodes[0]->m_rightNode, rightNumFrames); + } + + const float numFrames = static_cast(leftNumFrames + rightNumFrames); + const float halfFrames = numFrames / 2.0f; + const float balanceScore = 100.0f - (AZ::GetAbs(halfFrames - static_cast(leftNumFrames)) / numFrames) * 100.0f; + + // Get the maximum depth. + size_t maxDepth = 0; + for (const Node* node : m_nodes) + { + maxDepth = AZ::GetMax(maxDepth, node->m_dimension); + } + + AZ_TracePrintf("EMotionFX", "KdTree Balance Info: leftSide=%d rightSide=%d score=%.2f totalFrames=%d maxDepth=%d", leftNumFrames, rightNumFrames, balanceScore, leftNumFrames + rightNumFrames, maxDepth); + + size_t numLeafNodes = 0; + size_t numZeroNodes = 0; + size_t minFrames = 1000000000; + size_t maxFrames = 0; + for (const Node* node : m_nodes) + { + if (node->m_leftNode || node->m_rightNode) + { + continue; + } + + numLeafNodes++; + + if (node->m_frames.empty()) + { + numZeroNodes++; + } + + AZ_TracePrintf("EMotionFX", "Frames = %d", node->m_frames.size()); + + minFrames = AZ::GetMin(minFrames, node->m_frames.size()); + maxFrames = AZ::GetMax(maxFrames, node->m_frames.size()); + } + + const size_t avgFrames = (leftNumFrames + rightNumFrames) / numLeafNodes; + AZ_TracePrintf("EMotionFX", "KdTree Node Info: leafs=%d avgFrames=%d zeroFrames=%d minFrames=%d maxFrames=%d", numLeafNodes, avgFrames, numZeroNodes, minFrames, maxFrames); + } + + void KdTree::FindNearestNeighbors(const AZStd::vector& frameFloats, AZStd::vector& resultFrameIndices) const + { + AZ_Assert(IsInitialized() && !m_nodes.empty(), "Expecting a valid and initialized kdTree. Did you forget to call KdTree::Init()?"); + Node* curNode = m_nodes[0]; + + // Step as far as we need to through the kdTree. + Node* nodeToSearch = nullptr; + const size_t numDimensions = frameFloats.size(); + for (size_t d = 0; d < numDimensions; ++d) + { + AZ_Assert(curNode->m_dimension == d, "Dimension mismatch"); + + // We have children in both directions. + if (curNode->m_leftNode && curNode->m_rightNode) + { + curNode = (frameFloats[d] <= curNode->m_median) ? curNode->m_leftNode : curNode->m_rightNode; + } + else if (!curNode->m_leftNode && !curNode->m_rightNode) // we have a leaf node + { + nodeToSearch = curNode; + } + else + { + // We have both a left and right node, so we're not at a leaf yet. + if (curNode->m_leftNode) + { + if (frameFloats[d] <= curNode->m_median) + { + curNode = curNode->m_leftNode; + } + else + { + nodeToSearch = curNode; + } + } + else // We have a right node. + { + if (frameFloats[d] > curNode->m_median) + { + curNode = curNode->m_rightNode; + } + else + { + nodeToSearch = curNode; + } + } + } + + // If we found our search node, perform a linear search through the frames inside this node. + if (nodeToSearch) + { + //AZ_Assert(d == nodeToSearch->m_dimension, "Dimension mismatch inside kdTree nearest neighbor search."); + FindNearestNeighbors(nodeToSearch, frameFloats, resultFrameIndices); + return; + } + } + + FindNearestNeighbors(curNode, frameFloats, resultFrameIndices); + } + + void KdTree::FindNearestNeighbors([[maybe_unused]] Node* node, [[maybe_unused]] const AZStd::vector& frameFloats, AZStd::vector& resultFrameIndices) const + { + resultFrameIndices = node->m_frames; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/KdTree.h b/Gems/MotionMatching/Code/Source/KdTree.h new file mode 100644 index 0000000000..8b62788c32 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/KdTree.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class KdTree + { + public: + AZ_RTTI(KdTree, "{CDA707EC-4150-463B-8157-90D98351ACED}") + AZ_CLASS_ALLOCATOR_DECL + + KdTree() = default; + virtual ~KdTree(); + + bool Init(const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + 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. + */ + static size_t CalcNumDimensions(const AZStd::vector& features); + + void Clear(); + void PrintStats(); + + size_t GetNumNodes() const; + size_t GetNumDimensions() const; + size_t CalcMemoryUsageInBytes() const; + bool IsInitialized() const; + + void FindNearestNeighbors(const AZStd::vector& frameFloats, AZStd::vector& resultFrameIndices) const; + + private: + struct Node + { + Node* m_leftNode = nullptr; + Node* m_rightNode = nullptr; + Node* m_parent = nullptr; + float m_median = 0.0f; + size_t m_dimension = 0; + AZStd::vector m_frames; + }; + + void BuildTreeNodes(const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + Node* node, + Node* parent, + size_t dimension = 0, + bool leftSide = true); + void FillFeatureValues(const FeatureMatrix& featureMatrix, const Feature* feature, size_t frameIndex, size_t startIndex); + void FillFeatureValues(const FeatureMatrix& featureMatrix, const AZStd::vector& features, size_t frameIndex); + void FillFramesForNode(Node* node, + const FrameDatabase& frameDatabase, + const FeatureMatrix& featureMatrix, + const AZStd::vector& features, + Node* parent, + bool leftSide); + void RecursiveCalcNumFrames(Node* node, size_t& outNumFrames) const; + void ClearFramesForNonEssentialNodes(); + void MergeSmallLeafNodesToParents(); + void RemoveZeroFrameLeafNodes(); + void RemoveLeafNode(Node* node); + void FindNearestNeighbors(Node* node, const AZStd::vector& frameFloats, AZStd::vector& resultFrameIndices) const; + + private: + AZStd::vector m_nodes; + AZStd::vector m_featureValues; + size_t m_numDimensions = 0; + size_t m_maxDepth = 20; + size_t m_minFramesPerLeaf = 1000; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingData.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingData.cpp new file mode 100644 index 0000000000..1ce891c98b --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingData.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(MotionMatchingData, MotionMatchAllocator, 0) + + MotionMatchingData::MotionMatchingData(const FeatureSchema& featureSchema) + : m_featureSchema(featureSchema) + { + m_kdTree = AZStd::make_unique(); + } + + MotionMatchingData::~MotionMatchingData() + { + Clear(); + } + + bool MotionMatchingData::ExtractFeatures(ActorInstance* actorInstance, FrameDatabase* frameDatabase, size_t maxKdTreeDepth, size_t minFramesPerKdTreeNode) + { + AZ_PROFILE_SCOPE(Animation, "MotionMatchingData::ExtractFeatures"); + AZ::Debug::Timer timer; + timer.Stamp(); + + const size_t numFrames = frameDatabase->GetNumFrames(); + if (numFrames == 0) + { + return true; + } + + // Initialize all features before we process each frame. + FeatureMatrix::Index featureComponentCount = 0; + for (Feature* feature : m_featureSchema.GetFeatures()) + { + Feature::InitSettings frameSettings; + frameSettings.m_actorInstance = actorInstance; + if (!feature->Init(frameSettings)) + { + return false; + } + + feature->SetColumnOffset(featureComponentCount); + featureComponentCount += feature->GetNumDimensions(); + } + + const auto& frames = frameDatabase->GetFrames(); + + // Allocate memory for the feature matrix + m_featureMatrix.resize(/*rows=*/numFrames, /*columns=*/featureComponentCount); + + // Iterate over all frames and extract the data for this frame. + AnimGraphPosePool& posePool = GetEMotionFX().GetThreadData(actorInstance->GetThreadIndex())->GetPosePool(); + AnimGraphPose* pose = posePool.RequestPose(actorInstance); + + Feature::ExtractFeatureContext context(m_featureMatrix); + context.m_frameDatabase = frameDatabase; + context.m_framePose = &pose->GetPose(); + context.m_actorInstance = actorInstance; + + for (const Frame& frame : frames) + { + context.m_frameIndex = frame.GetFrameIndex(); + + // Pre-sample the frame pose as that will be needed by many of the feature extraction calculations. + frame.SamplePose(const_cast(context.m_framePose)); + + // Extract all features for the given frame. + { + for (Feature* feature : m_featureSchema.GetFeatures()) + { + feature->ExtractFeatureValues(context); + } + } + } + + posePool.FreePose(pose); + + const float extractFeaturesTime = timer.GetDeltaTimeInSeconds(); + timer.Stamp(); + + // Initialize the kd-tree used to accelerate the searches. + if (!m_kdTree->Init(*frameDatabase, m_featureMatrix, m_featuresInKdTree, maxKdTreeDepth, minFramesPerKdTreeNode)) // Internally automatically clears any existing contents. + { + AZ_Error("EMotionFX", false, "Failed to initialize KdTree acceleration structure."); + return false; + } + + const float initKdTreeTimer = timer.GetDeltaTimeInSeconds(); + + AZ_Printf("MotionMatching", "Feature matrix (%zu, %zu) uses %.2f MB and took %.2f ms to initialize (KD-Tree %.2f ms).", + m_featureMatrix.rows(), + m_featureMatrix.cols(), + static_cast(m_featureMatrix.CalcMemoryUsageInBytes()) / 1024.0f / 1024.0f, + extractFeaturesTime * 1000.0f, + initKdTreeTimer * 1000.0f); + + return true; + } + + bool MotionMatchingData::Init(const InitSettings& settings) + { + AZ_PROFILE_SCOPE(Animation, "MotionMatchingData::Init"); + + // Import all motion frames. + size_t totalNumFramesImported = 0; + size_t totalNumFramesDiscarded = 0; + for (Motion* motion : settings.m_motionList) + { + size_t numFrames = 0; + size_t numDiscarded = 0; + std::tie(numFrames, numDiscarded) = m_frameDatabase.ImportFrames(motion, settings.m_frameImportSettings, false); + totalNumFramesImported += numFrames; + totalNumFramesDiscarded += numDiscarded; + + if (settings.m_importMirrored) + { + std::tie(numFrames, numDiscarded) = m_frameDatabase.ImportFrames(motion, settings.m_frameImportSettings, true); + totalNumFramesImported += numFrames; + totalNumFramesDiscarded += numDiscarded; + } + } + + if (totalNumFramesImported > 0 || totalNumFramesDiscarded > 0) + { + AZ_TracePrintf("Motion Matching", "Imported a total of %d frames (%d frames discarded) across %d motions. This is %.2f seconds (%.2f minutes) of motion data.", + totalNumFramesImported, + totalNumFramesDiscarded, + settings.m_motionList.size(), + totalNumFramesImported / (float)settings.m_frameImportSettings.m_sampleRate, + (totalNumFramesImported / (float)settings.m_frameImportSettings.m_sampleRate) / 60.0f); + } + + // Use all features other than the trajectory for the broad-phase search using the KD-Tree. + for (Feature* feature : m_featureSchema.GetFeatures()) + { + if (feature->RTTI_GetType() != azrtti_typeid()) + { + m_featuresInKdTree.push_back(feature); + } + } + + // Extract feature data and place the values into the feature matrix. + if (!ExtractFeatures(settings.m_actorInstance, &m_frameDatabase, settings.m_maxKdTreeDepth, settings.m_minFramesPerKdTreeNode)) + { + AZ_Error("Motion Matching", false, "Failed to extract features from motion database."); + return false; + } + + return true; + } + + void MotionMatchingData::Clear() + { + m_frameDatabase.Clear(); + m_featureMatrix.Clear(); + m_kdTree->Clear(); + m_featuresInKdTree.clear(); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingData.h b/Gems/MotionMatching/Code/Source/MotionMatchingData.h new file mode 100644 index 0000000000..15748bb849 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingData.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace EMotionFX +{ + class ActorInstance; +} + +namespace EMotionFX::MotionMatching +{ + class EMFX_API MotionMatchingData + { + public: + AZ_RTTI(MotionMatchingData, "{7BC3DFF5-8864-4518-B6F0-0553ADFAB5C1}") + AZ_CLASS_ALLOCATOR_DECL + + MotionMatchingData(const FeatureSchema& featureSchema); + virtual ~MotionMatchingData(); + + struct EMFX_API InitSettings + { + ActorInstance* m_actorInstance = nullptr; + AZStd::vector m_motionList; + FrameDatabase::FrameImportSettings m_frameImportSettings; + size_t m_maxKdTreeDepth = 20; + size_t m_minFramesPerKdTreeNode = 1000; + bool m_importMirrored = false; + }; + bool Init(const InitSettings& settings); + + void Clear(); + + const FrameDatabase& GetFrameDatabase() const { return m_frameDatabase; } + FrameDatabase& GetFrameDatabase() { return m_frameDatabase; } + const FeatureSchema& GetFeatureSchema() const { return m_featureSchema; } + const FeatureMatrix& GetFeatureMatrix() const { return m_featureMatrix; } + const KdTree& GetKdTree() const { return *m_kdTree.get(); } + const AZStd::vector& GetFeaturesInKdTree() const { return m_featuresInKdTree; } + + 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. */ + + 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::vector m_featuresInKdTree; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingEditorModule.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingEditorModule.cpp new file mode 100644 index 0000000000..fce6957901 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingEditorModule.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingEditorModule + : public MotionMatchingModuleInterface + { + public: + AZ_RTTI(MotionMatchingEditorModule, "{cf4381d1-0207-4ef8-85f0-6c88ec28a7b6}", MotionMatchingModuleInterface); + AZ_CLASS_ALLOCATOR(MotionMatchingEditorModule, AZ::SystemAllocator, 0); + + MotionMatchingEditorModule() + { + m_descriptors.insert(m_descriptors.end(), + { + MotionMatchingEditorSystemComponent::CreateDescriptor(), + }); + } + + /// Add required SystemComponents to the SystemEntity. Non-SystemComponents should not be added here. + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList + { + azrtti_typeid(), + }; + } + }; +}// namespace EMotionFX::MotionMatching + +AZ_DECLARE_MODULE_CLASS(Gem_MotionMatching, EMotionFX::MotionMatching::MotionMatchingEditorModule) diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.cpp new file mode 100644 index 0000000000..d8f0079a59 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +namespace EMotionFX::MotionMatching +{ + void MotionMatchingEditorSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0); + } + } + + MotionMatchingEditorSystemComponent::MotionMatchingEditorSystemComponent() = default; + + MotionMatchingEditorSystemComponent::~MotionMatchingEditorSystemComponent() = default; + + void MotionMatchingEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + BaseSystemComponent::GetProvidedServices(provided); + provided.push_back(AZ_CRC_CE("MotionMatchingEditorService")); + } + + void MotionMatchingEditorSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + BaseSystemComponent::GetIncompatibleServices(incompatible); + incompatible.push_back(AZ_CRC_CE("MotionMatchingEditorService")); + } + + void MotionMatchingEditorSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + BaseSystemComponent::GetRequiredServices(required); + } + + void MotionMatchingEditorSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + BaseSystemComponent::GetDependentServices(dependent); + } + + void MotionMatchingEditorSystemComponent::Activate() + { + MotionMatchingSystemComponent::Activate(); + AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); + } + + void MotionMatchingEditorSystemComponent::Deactivate() + { + AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + MotionMatchingSystemComponent::Deactivate(); + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.h b/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.h new file mode 100644 index 0000000000..a9d3bb528f --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingEditorSystemComponent.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +namespace EMotionFX::MotionMatching +{ + /// System component for MotionMatching editor + class MotionMatchingEditorSystemComponent + : public MotionMatchingSystemComponent + , private AzToolsFramework::EditorEvents::Bus::Handler + { + using BaseSystemComponent = MotionMatchingSystemComponent; + public: + AZ_COMPONENT(MotionMatchingEditorSystemComponent, "{a43957d3-5a2d-4c29-873d-7daacc357722}", BaseSystemComponent); + static void Reflect(AZ::ReflectContext* context); + + MotionMatchingEditorSystemComponent(); + ~MotionMatchingEditorSystemComponent(); + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + // AZ::Component + void Activate() override; + void Deactivate() override; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingInstance.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingInstance.cpp new file mode 100644 index 0000000000..54fc2918e6 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingInstance.cpp @@ -0,0 +1,571 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(MotionMatchingInstance, MotionMatchAllocator, 0) + + MotionMatchingInstance::~MotionMatchingInstance() + { + if (m_motionInstance) + { + GetMotionInstancePool().Free(m_motionInstance); + } + + if (m_prevMotionInstance) + { + GetMotionInstancePool().Free(m_prevMotionInstance); + } + } + + MotionInstance* MotionMatchingInstance::CreateMotionInstance() const + { + MotionInstance* result = GetMotionInstancePool().RequestNew(m_data->GetFrameDatabase().GetFrame(0).GetSourceMotion(), m_actorInstance); + return result; + } + + void MotionMatchingInstance::Init(const InitSettings& settings) + { + AZ_Assert(settings.m_actorInstance, "The actor instance cannot be a nullptr."); + AZ_Assert(settings.m_data, "The motion match data cannot be nullptr."); + + // Update the cached pointer to the trajectory feature. + const FeatureSchema& featureSchema = settings.m_data->GetFeatureSchema(); + for (Feature* feature : featureSchema.GetFeatures()) + { + if (feature->RTTI_GetType() == azrtti_typeid()) + { + m_cachedTrajectoryFeature = static_cast(feature); + break; + } + } + + // Debug display initialization. + const auto AddDebugDisplay = [=](AZ::s32 debugDisplayId) + { + if (debugDisplayId == -1) + { + return; + } + + AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; + AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, debugDisplayId); + + AzFramework::DebugDisplayRequests* debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); + if (debugDisplay) + { + m_debugDisplays.emplace_back(debugDisplay); + } + }; + // Draw the debug visualizations to the Animation Editor as well as the LY Editor viewport. + AZ::s32 animationEditorViewportId = -1; + EMStudio::ViewportPluginRequestBus::BroadcastResult(animationEditorViewportId, &EMStudio::ViewportPluginRequestBus::Events::GetViewportId); + AddDebugDisplay(animationEditorViewportId); + AddDebugDisplay(AzFramework::g_defaultSceneEntityDebugDisplayId); + + m_actorInstance = settings.m_actorInstance; + m_data = settings.m_data; + if (settings.m_data->GetFrameDatabase().GetNumFrames() == 0) + { + return; + } + + if (!m_motionInstance) + { + m_motionInstance = CreateMotionInstance(); + } + + if (!m_prevMotionInstance) + { + m_prevMotionInstance = CreateMotionInstance(); + } + + m_blendSourcePose.LinkToActorInstance(m_actorInstance); + m_blendSourcePose.InitFromBindPose(m_actorInstance); + + m_blendTargetPose.LinkToActorInstance(m_actorInstance); + m_blendTargetPose.InitFromBindPose(m_actorInstance); + + m_queryPose.LinkToActorInstance(m_actorInstance); + m_queryPose.InitFromBindPose(m_actorInstance); + + // Make sure we have enough space inside the frame floats array, which is used to search the kdTree. + const size_t numValuesInKdTree = m_data->GetKdTree().GetNumDimensions(); + m_queryFeatureValues.resize(numValuesInKdTree); + + // Initialize the trajectory history. + size_t rootJointIndex = m_actorInstance->GetActor()->GetMotionExtractionNodeIndex(); + if (rootJointIndex == InvalidIndex32) + { + rootJointIndex = 0; + } + m_trajectoryHistory.Init(*m_actorInstance->GetTransformData()->GetCurrentPose(), + rootJointIndex, + m_cachedTrajectoryFeature->GetFacingAxisDir(), + m_trajectorySecsToTrack); + } + + void MotionMatchingInstance::DebugDraw() + { + if (m_data && !m_debugDisplays.empty()) + { + for (AzFramework::DebugDisplayRequests* debugDisplay : m_debugDisplays) + { + if (debugDisplay) + { + const AZ::u32 prevState = debugDisplay->GetState(); + DebugDraw(*debugDisplay); + debugDisplay->SetState(prevState); + } + } + } + } + + void MotionMatchingInstance::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay) + { + AZ_PROFILE_SCOPE(Animation, "MotionMatchingInstance::DebugDraw"); + + // Get the lowest cost frame index from the last search. As we're searching the feature database with a much lower + // frequency and sample the animation onwards from this, the resulting frame index does not represent the current + // feature values from the shown pose. + const size_t curFrameIndex = GetLowestCostFrameIndex(); + if (curFrameIndex == InvalidIndex) + { + return; + } + + const FrameDatabase& frameDatabase = m_data->GetFrameDatabase(); + const FeatureSchema& featureSchema = m_data->GetFeatureSchema(); + + // Find the frame index in the frame database that belongs to the currently used pose. + const size_t currentFrame = frameDatabase.FindFrameIndex(m_motionInstance->GetMotion(), m_motionInstance->GetCurrentTime()); + + // Render the feature debug visualizations for the current frame. + if (currentFrame != InvalidIndex) + { + for (Feature* feature: featureSchema.GetFeatures()) + { + if (feature->GetDebugDrawEnabled()) + { + feature->DebugDraw(debugDisplay, this, currentFrame); + } + } + } + + // Draw the desired future trajectory and the sampled version of the past trajectory. + const AZ::Color trajectoryQueryColor = AZ::Color::CreateFromRgba(90,219,64,255); + m_trajectoryQuery.DebugDraw(debugDisplay, trajectoryQueryColor); + + // Draw the trajectory history starting after the sampled version of the past trajectory. + m_trajectoryHistory.DebugDraw(debugDisplay, trajectoryQueryColor, m_cachedTrajectoryFeature->GetPastTimeRange()); + } + + void MotionMatchingInstance::SamplePose(MotionInstance* motionInstance, Pose& outputPose) + { + const Pose* bindPose = m_actorInstance->GetTransformData()->GetBindPose(); + motionInstance->GetMotion()->Update(bindPose, &outputPose, motionInstance); + if (m_actorInstance->GetActor()->GetMotionExtractionNode() && m_actorInstance->GetMotionExtractionEnabled()) + { + outputPose.CompensateForMotionExtraction(); + } + } + + void MotionMatchingInstance::SamplePose(Motion* motion, Pose& outputPose, float sampleTime) const + { + MotionDataSampleSettings sampleSettings; + sampleSettings.m_actorInstance = outputPose.GetActorInstance(); + sampleSettings.m_inPlace = false; + sampleSettings.m_mirror = false; + sampleSettings.m_retarget = false; + sampleSettings.m_inputPose = sampleSettings.m_actorInstance->GetTransformData()->GetBindPose(); + + sampleSettings.m_sampleTime = sampleTime; + sampleSettings.m_sampleTime = AZ::GetClamp(sampleTime, 0.0f, motion->GetDuration()); + + motion->SamplePose(&outputPose, sampleSettings); + } + + void MotionMatchingInstance::PostUpdate([[maybe_unused]] float timeDelta) + { + if (!m_data) + { + m_motionExtractionDelta.Identity(); + return; + } + + const size_t lowestCostFrame = GetLowestCostFrameIndex(); + if (m_data->GetFrameDatabase().GetNumFrames() == 0 || lowestCostFrame == InvalidIndex) + { + m_motionExtractionDelta.Identity(); + return; + } + + // Blend the motion extraction deltas. + // Note: Make sure to update the previous as well as the current/target motion instances. + if (m_blendWeight >= 1.0f - AZ::Constants::FloatEpsilon) + { + m_motionInstance->ExtractMotion(m_motionExtractionDelta); + } + else if (m_blendWeight > AZ::Constants::FloatEpsilon && m_blendWeight < 1.0f - AZ::Constants::FloatEpsilon) + { + Transform targetMotionExtractionDelta; + m_motionInstance->ExtractMotion(m_motionExtractionDelta); + m_prevMotionInstance->ExtractMotion(targetMotionExtractionDelta); + m_motionExtractionDelta.Blend(targetMotionExtractionDelta, m_blendWeight); + } + else + { + m_prevMotionInstance->ExtractMotion(m_motionExtractionDelta); + } + } + + void MotionMatchingInstance::Output(Pose& outputPose) + { + AZ_PROFILE_SCOPE(Animation, "MotionMatchingInstance::Output"); + + if (!m_data) + { + outputPose.InitFromBindPose(m_actorInstance); + return; + } + + const size_t lowestCostFrame = GetLowestCostFrameIndex(); + if (m_data->GetFrameDatabase().GetNumFrames() == 0 || lowestCostFrame == InvalidIndex) + { + outputPose.InitFromBindPose(m_actorInstance); + return; + } + + // Sample the motions and blend the results when needed. + if (m_blendWeight >= 1.0f - AZ::Constants::FloatEpsilon) + { + m_blendTargetPose.InitFromBindPose(m_actorInstance); + if (m_motionInstance) + { + SamplePose(m_motionInstance, m_blendTargetPose); + } + outputPose = m_blendTargetPose; + } + else if (m_blendWeight > AZ::Constants::FloatEpsilon && m_blendWeight < 1.0f - AZ::Constants::FloatEpsilon) + { + m_blendSourcePose.InitFromBindPose(m_actorInstance); + m_blendTargetPose.InitFromBindPose(m_actorInstance); + if (m_motionInstance) + { + SamplePose(m_motionInstance, m_blendTargetPose); + } + if (m_prevMotionInstance) + { + SamplePose(m_prevMotionInstance, m_blendSourcePose); + } + + outputPose = m_blendSourcePose; + outputPose.Blend(&m_blendTargetPose, m_blendWeight); + } + else + { + m_blendSourcePose.InitFromBindPose(m_actorInstance); + if (m_prevMotionInstance) + { + SamplePose(m_prevMotionInstance, m_blendSourcePose); + } + outputPose = m_blendSourcePose; + } + } + + void MotionMatchingInstance::Update(float timePassedInSeconds, const AZ::Vector3& targetPos, const AZ::Vector3& targetFacingDir, TrajectoryQuery::EMode mode, float pathRadius, float pathSpeed) + { + AZ_PROFILE_SCOPE(Animation, "MotionMatchingInstance::Update"); + + if (!m_data) + { + return; + } + + size_t currentFrameIndex = GetLowestCostFrameIndex(); + if (currentFrameIndex == InvalidIndex) + { + currentFrameIndex = 0; + } + + // Add the sample from the last frame (post-motion extraction) + m_trajectoryHistory.AddSample(*m_actorInstance->GetTransformData()->GetCurrentPose()); + // Update the time. After this there is no sample for the updated time in the history as we're about to prepare this with the current update. + m_trajectoryHistory.Update(timePassedInSeconds); + + // Register the current actor instance position to the history data of the spline. + m_trajectoryQuery.Update(m_actorInstance, + m_cachedTrajectoryFeature, + m_trajectoryHistory, + mode, + targetPos, + targetFacingDir, + timePassedInSeconds, + pathRadius, + pathSpeed); + + // Calculate the new time value of the motion, but don't set it yet (the syncing might adjust this again) + m_motionInstance->SetFreezeAtLastFrame(true); + m_motionInstance->SetMaxLoops(1); + const float newMotionTime = m_motionInstance->CalcPlayStateAfterUpdate(timePassedInSeconds).m_currentTime; + m_newMotionTime = newMotionTime; + + // Keep on playing the previous instance as we're blending the poses and motion extraction deltas. + m_prevMotionInstance->Update(timePassedInSeconds); + + m_timeSinceLastFrameSwitch += timePassedInSeconds; + + const float lowestCostSearchTimeInterval = 1.0f / m_lowestCostSearchFrequency; + + if (m_blending) + { + const float maxBlendTime = lowestCostSearchTimeInterval; + m_blendProgressTime += timePassedInSeconds; + if (m_blendProgressTime > maxBlendTime) + { + m_blendWeight = 1.0f; + m_blendProgressTime = maxBlendTime; + m_blending = false; + } + else + { + m_blendWeight = AZ::GetClamp(m_blendProgressTime / maxBlendTime, 0.0f, 1.0f); + } + } + + const bool searchLowestCostFrame = m_timeSinceLastFrameSwitch >= lowestCostSearchTimeInterval; + if (searchLowestCostFrame) + { + // Calculate the input query pose for the motion matching search algorithm. + { + // Sample the pose for the new motion time as the motion instance has not been updated with the timeDelta from this frame yet. + SamplePose(m_motionInstance->GetMotion(), m_queryPose, newMotionTime); + + // Copy over the motion extraction joint transform from the current pose to the newly sampled pose. + // When sampling a motion, the motion extraction joint is in animation space, while we need the query pose to be in + // world space. + // Note: This does not yet take the extraction delta from the current tick into account. + if (m_actorInstance->GetActor()->GetMotionExtractionNode()) + { + const Pose* currentPose = m_actorInstance->GetTransformData()->GetCurrentPose(); + const size_t motionExtractionJointIndex = m_actorInstance->GetActor()->GetMotionExtractionNodeIndex(); + m_queryPose.SetWorldSpaceTransform(motionExtractionJointIndex, + currentPose->GetWorldSpaceTransform(motionExtractionJointIndex)); + } + + // Calculate the joint velocities for the sampled pose using the same method as we do for the frame database. + PoseDataJointVelocities* velocityPoseData = m_queryPose.GetAndPreparePoseData(m_actorInstance); + velocityPoseData->CalculateVelocity(m_motionInstance, m_cachedTrajectoryFeature->GetRelativeToNodeIndex()); + } + + const FeatureMatrix& featureMatrix = m_data->GetFeatureMatrix(); + const FrameDatabase& frameDatabase = m_data->GetFrameDatabase(); + + Feature::FrameCostContext frameCostContext(featureMatrix, m_queryPose); + frameCostContext.m_trajectoryQuery = &m_trajectoryQuery; + frameCostContext.m_actorInstance = m_actorInstance; + const size_t lowestCostFrameIndex = FindLowestCostFrameIndex(frameCostContext); + + const Frame& currentFrame = frameDatabase.GetFrame(currentFrameIndex); + const Frame& lowestCostFrame = frameDatabase.GetFrame(lowestCostFrameIndex); + const bool sameMotion = (currentFrame.GetSourceMotion() == lowestCostFrame.GetSourceMotion()); + const float timeBetweenFrames = newMotionTime - lowestCostFrame.GetSampleTime(); + const bool sameLocation = sameMotion && (AZ::GetAbs(timeBetweenFrames) < 0.1f); + + if (lowestCostFrameIndex != currentFrameIndex && !sameLocation) + { + // Start a blend. + m_blending = true; + m_blendWeight = 0.0f; + m_blendProgressTime = 0.0f; + + // Store the current motion instance state, so we can sample this as source pose. + m_prevMotionInstance->SetMotion(m_motionInstance->GetMotion()); + m_prevMotionInstance->SetMirrorMotion(m_motionInstance->GetMirrorMotion()); + m_prevMotionInstance->SetCurrentTime(newMotionTime, true); + m_prevMotionInstance->SetLastCurrentTime(m_prevMotionInstance->GetCurrentTime() - timePassedInSeconds); + + m_lowestCostFrameIndex = lowestCostFrameIndex; + + m_motionInstance->SetMotion(lowestCostFrame.GetSourceMotion()); + m_motionInstance->SetMirrorMotion(lowestCostFrame.GetMirrored()); + + // The new motion time will become the current time after this frame while the current time + // becomes the last current time. As we just start playing at the search frame, calculate + // the last time based on the time delta. + m_motionInstance->SetCurrentTime(lowestCostFrame.GetSampleTime() - timePassedInSeconds, true); + m_newMotionTime = lowestCostFrame.GetSampleTime(); + } + + // Do this always, else wise we search for the lowest cost frame index too many times. + m_timeSinceLastFrameSwitch = 0.0f; + } + + // ImGui monitor + { +#ifdef IMGUI_ENABLED + const KdTree& kdTree = m_data->GetKdTree(); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetKdTreeMemoryUsage, kdTree.CalcMemoryUsageInBytes()); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetKdTreeNumNodes, kdTree.GetNumNodes()); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetKdTreeNumDimensions, kdTree.GetNumDimensions()); + // TODO: add memory usage for frame database + + const FeatureMatrix& featureMatrix = m_data->GetFeatureMatrix(); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetFeatureMatrixMemoryUsage, featureMatrix.CalcMemoryUsageInBytes()); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetFeatureMatrixNumFrames, featureMatrix.rows()); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::SetFeatureMatrixNumComponents, featureMatrix.cols()); +#endif + } + } + + size_t MotionMatchingInstance::FindLowestCostFrameIndex(const Feature::FrameCostContext& context) + { + AZ::Debug::Timer timer; + timer.Stamp(); + + AZ_PROFILE_SCOPE(Animation, "MotionMatchingInstance::FindLowestCostFrameIndex"); + + const FrameDatabase& frameDatabase = m_data->GetFrameDatabase(); + const FeatureSchema& featureSchema = m_data->GetFeatureSchema(); + const FeatureTrajectory* trajectoryFeature = m_cachedTrajectoryFeature; + + // 1. Broad-phase search using KD-tree + { + // Build the input query features that will be compared to every entry in the feature database in the motion matching search. + size_t startOffset = 0; + for (Feature* feature : m_data->GetFeaturesInKdTree()) + { + feature->FillQueryFeatureValues(startOffset, m_queryFeatureValues, context); + startOffset += feature->GetNumDimensions(); + } + AZ_Assert(startOffset == m_queryFeatureValues.size(), "Frame float vector is not the expected size."); + + // Find our nearest frames. + m_data->GetKdTree().FindNearestNeighbors(m_queryFeatureValues, m_nearestFrames); + } + + // 2. Narrow-phase, brute force find the actual best matching frame (frame with the minimal cost). + float minCost = FLT_MAX; + size_t minCostFrameIndex = 0; + m_tempCosts.resize(featureSchema.GetNumFeatures()); + m_minCosts.resize(featureSchema.GetNumFeatures()); + float minTrajectoryPastCost = 0.0f; + float minTrajectoryFutureCost = 0.0f; + + // Iterate through the frames filtered by the broad-phase search. + for (const size_t frameIndex : m_nearestFrames) + { + const Frame& frame = frameDatabase.GetFrame(frameIndex); + + // TODO: This shouldn't be there, we should be discarding the frames when extracting the features and not at runtime when checking the cost. + if (frame.GetSampleTime() >= frame.GetSourceMotion()->GetDuration() - 1.0f) + { + continue; + } + + float frameCost = 0.0f; + + // Calculate the frame cost by accumulating the weighted feature costs. + for (size_t featureIndex = 0; featureIndex < featureSchema.GetNumFeatures(); ++featureIndex) + { + Feature* feature = featureSchema.GetFeature(featureIndex); + if (feature->RTTI_GetType() != azrtti_typeid()) + { + const float featureCost = feature->CalculateFrameCost(frameIndex, context); + const float featureCostFactor = feature->GetCostFactor(); + const float featureFinalCost = featureCost * featureCostFactor; + + frameCost += featureFinalCost; + m_tempCosts[featureIndex] = featureFinalCost; + } + } + + // Manually add the trajectory cost. + float trajectoryPastCost = 0.0f; + float trajectoryFutureCost = 0.0f; + if (trajectoryFeature) + { + trajectoryPastCost = trajectoryFeature->CalculatePastFrameCost(frameIndex, context) * trajectoryFeature->GetPastCostFactor(); + trajectoryFutureCost = trajectoryFeature->CalculateFutureFrameCost(frameIndex, context) * trajectoryFeature->GetFutureCostFactor(); + frameCost += trajectoryPastCost; + frameCost += trajectoryFutureCost; + } + + // Track the minimum feature and frame costs. + if (frameCost < minCost) + { + minCost = frameCost; + minCostFrameIndex = frameIndex; + + for (size_t featureIndex = 0; featureIndex < featureSchema.GetNumFeatures(); ++featureIndex) + { + Feature* feature = featureSchema.GetFeature(featureIndex); + if (feature->RTTI_GetType() != azrtti_typeid()) + { + m_minCosts[featureIndex] = m_tempCosts[featureIndex]; + } + } + + minTrajectoryPastCost = trajectoryPastCost; + minTrajectoryFutureCost = trajectoryFutureCost; + } + } + + // 3. ImGui debug visualization + { + const float time = timer.GetDeltaTimeInSeconds(); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "FindLowestCostFrameIndex", time * 1000.0f); + + for (size_t featureIndex = 0; featureIndex < featureSchema.GetNumFeatures(); ++featureIndex) + { + Feature* feature = featureSchema.GetFeature(featureIndex); + if (feature->RTTI_GetType() != azrtti_typeid()) + { + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushCostHistogramValue, + feature->GetName().c_str(), + m_minCosts[featureIndex], + feature->GetDebugDrawColor()); + } + } + + if (trajectoryFeature) + { + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushCostHistogramValue, "Future Trajectory", minTrajectoryFutureCost, trajectoryFeature->GetDebugDrawColor()); + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushCostHistogramValue, "Past Trajectory", minTrajectoryPastCost, trajectoryFeature->GetDebugDrawColor()); + } + + ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushCostHistogramValue, "Total Cost", minCost, AZ::Color::CreateFromRgba(202,255,191,255)); + } + + return minCostFrameIndex; + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingInstance.h b/Gems/MotionMatching/Code/Source/MotionMatchingInstance.h new file mode 100644 index 0000000000..49c781162d --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingInstance.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace EMotionFX +{ + class ActorInstance; + class Motion; +} + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingData; + + class EMFX_API MotionMatchingInstance + { + public: + AZ_RTTI(MotionMatchingInstance, "{1ED03AD8-0FB2-431B-AF01-02F7E930EB73}") + AZ_CLASS_ALLOCATOR_DECL + + virtual ~MotionMatchingInstance(); + + struct EMFX_API InitSettings + { + ActorInstance* m_actorInstance = nullptr; + MotionMatchingData* m_data = nullptr; + }; + void Init(const InitSettings& settings); + + void DebugDraw(); + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay); + + void Update(float timePassedInSeconds, const AZ::Vector3& targetPos, const AZ::Vector3& targetFacingDir, TrajectoryQuery::EMode mode, float pathRadius, float pathSpeed); + void PostUpdate(float timeDelta); + void Output(Pose& outputPose); + + MotionInstance* GetMotionInstance() const { return m_motionInstance; } + ActorInstance* GetActorInstance() const { return m_actorInstance; } + MotionMatchingData* GetData() const { return m_data; } + + size_t GetLowestCostFrameIndex() const { return m_lowestCostFrameIndex; } + void SetLowestCostSearchFrequency(float frequency) { m_lowestCostSearchFrequency = frequency; } + float GetNewMotionTime() const { return m_newMotionTime; } + + /** + * Get the cached trajectory feature. + * The trajectory feature is searched in the feature schema used in the current instance at init time. + */ + FeatureTrajectory* GetTrajectoryFeature() const { return m_cachedTrajectoryFeature; } + const TrajectoryQuery& GetTrajectoryQuery() const { return m_trajectoryQuery; } + const TrajectoryHistory& GetTrajectoryHistory() const { return m_trajectoryHistory; } + const Transform& GetMotionExtractionDelta() const { return m_motionExtractionDelta; } + + private: + MotionInstance* CreateMotionInstance() const; + void SamplePose(MotionInstance* motionInstance, Pose& outputPose); + void SamplePose(Motion* motion, Pose& outputPose, float sampleTime) const; + + size_t FindLowestCostFrameIndex(const Feature::FrameCostContext& context); + + MotionMatchingData* m_data = nullptr; + ActorInstance* m_actorInstance = nullptr; + Pose m_blendSourcePose; + Pose m_blendTargetPose; + Pose m_queryPose; //! Input query pose for the motion matching search. + MotionInstance* m_motionInstance = nullptr; + MotionInstance* m_prevMotionInstance = nullptr; + Transform m_motionExtractionDelta = Transform::CreateIdentity(); + + /// Buffers used for the broad-phase KD-tree search. + AZStd::vector m_queryFeatureValues; /** The input query features to be compared to every entry/row in the feature matrix with the motion matching search. */ + AZStd::vector m_nearestFrames; /** Stores the nearest matching frames / search result from the KD-tree. */ + + FeatureTrajectory* m_cachedTrajectoryFeature = nullptr; /** Cached pointer to the trajectory feature in the feature schema. */ + TrajectoryQuery m_trajectoryQuery; + TrajectoryHistory m_trajectoryHistory; + static constexpr float m_trajectorySecsToTrack = 5.0f; + + float m_timeSinceLastFrameSwitch = 0.0f; + float m_newMotionTime = 0.0f; + size_t m_lowestCostFrameIndex = InvalidIndex; + float m_lowestCostSearchFrequency = 5.0f; //< How often the lowest cost frame shall be searched per second. + + bool m_blending = false; + float m_blendWeight = 1.0f; + float m_blendProgressTime = 0.0f; // How long are we already blending? In seconds. + + /// Buffers used for FindLowestCostFrameIndex(). + AZStd::vector m_tempCosts; + AZStd::vector m_minCosts; + + AZStd::vector m_debugDisplays; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingModule.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingModule.cpp new file mode 100644 index 0000000000..bc29172bb5 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingModule.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingModule + : public MotionMatchingModuleInterface + { + public: + AZ_RTTI(MotionMatchingModule, "{cf4381d1-0207-4ef8-85f0-6c88ec28a7b6}", MotionMatchingModuleInterface); + AZ_CLASS_ALLOCATOR(MotionMatchingModule, AZ::SystemAllocator, 0); + }; +}// namespace EMotionFX::MotionMatching + +AZ_DECLARE_MODULE_CLASS(Gem_MotionMatching, EMotionFX::MotionMatching::MotionMatchingModule) diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingModuleInterface.h b/Gems/MotionMatching/Code/Source/MotionMatchingModuleInterface.h new file mode 100644 index 0000000000..e2110263f5 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingModuleInterface.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingModuleInterface + : public AZ::Module + { + public: + AZ_RTTI(MotionMatchingModuleInterface, "{33e8e826-b143-4008-89f3-9a46ad3de4fe}", AZ::Module); + AZ_CLASS_ALLOCATOR(MotionMatchingModuleInterface, AZ::SystemAllocator, 0); + + MotionMatchingModuleInterface() + { + m_descriptors.insert(m_descriptors.end(), + { + MotionMatchingSystemComponent::CreateDescriptor(), + }); + } + + /// Add required SystemComponents to the SystemEntity. + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList + { + azrtti_typeid(), + }; + } + }; +}// namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.cpp b/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.cpp new file mode 100644 index 0000000000..073362d741 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + void MotionMatchingSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ; + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("MotionMatching", "[Description of functionality provided by this System Component]") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ; + } + } + + EMotionFX::MotionMatching::DiscardFrameEventData::Reflect(context); + EMotionFX::MotionMatching::TagEventData::Reflect(context); + + EMotionFX::MotionMatching::FeatureSchema::Reflect(context); + EMotionFX::MotionMatching::Feature::Reflect(context); + EMotionFX::MotionMatching::FeaturePosition::Reflect(context); + EMotionFX::MotionMatching::FeatureTrajectory::Reflect(context); + EMotionFX::MotionMatching::FeatureVelocity::Reflect(context); + + EMotionFX::MotionMatching::PoseDataJointVelocities::Reflect(context); + + EMotionFX::MotionMatching::BlendTreeMotionMatchNode::Reflect(context); + } + + void MotionMatchingSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("MotionMatchingService")); + } + + void MotionMatchingSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("MotionMatchingService")); + } + + void MotionMatchingSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC("EMotionFXAnimationService", 0x3f8a6369)); + } + + void MotionMatchingSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + MotionMatchingSystemComponent::MotionMatchingSystemComponent() + { + if (MotionMatchingInterface::Get() == nullptr) + { + MotionMatchingInterface::Register(this); + } + } + + MotionMatchingSystemComponent::~MotionMatchingSystemComponent() + { + if (MotionMatchingInterface::Get() == this) + { + MotionMatchingInterface::Unregister(this); + } + } + + void MotionMatchingSystemComponent::Init() + { + } + + void MotionMatchingSystemComponent::Activate() + { + MotionMatchingRequestBus::Handler::BusConnect(); + AZ::TickBus::Handler::BusConnect(); + + // Register the motion matching anim graph node + EMotionFX::AnimGraphObject* motionMatchNodeObject = EMotionFX::AnimGraphObjectFactory::Create(azrtti_typeid()); + auto motionMatchNode = azdynamic_cast(motionMatchNodeObject); + if (motionMatchNode) + { + EMotionFX::Integration::EMotionFXRequestBus::Broadcast(&EMotionFX::Integration::EMotionFXRequests::RegisterAnimGraphObjectType, motionMatchNode); + delete motionMatchNode; + } + + // Register the joint velocities pose data. + EMotionFX::GetPoseDataFactory().AddPoseDataType(azrtti_typeid()); + } + + void MotionMatchingSystemComponent::Deactivate() + { + AZ::TickBus::Handler::BusDisconnect(); + MotionMatchingRequestBus::Handler::BusDisconnect(); + } + + void MotionMatchingSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + { + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.h b/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.h new file mode 100644 index 0000000000..6a5b5a7b73 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/MotionMatchingSystemComponent.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class MotionMatchingSystemComponent + : public AZ::Component + , protected MotionMatchingRequestBus::Handler + , public AZ::TickBus::Handler + { + public: + AZ_COMPONENT(MotionMatchingSystemComponent, "{158cd35c-b548-4d7b-9493-9a3c5c359e49}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + MotionMatchingSystemComponent(); + ~MotionMatchingSystemComponent(); + + protected: + //////////////////////////////////////////////////////////////////////// + // MotionMatchingRequestBus interface implementation + + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // AZTickBus interface implementation + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + //////////////////////////////////////////////////////////////////////// + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.cpp b/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.cpp new file mode 100644 index 0000000000..b74e8d9df7 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ_CLASS_ALLOCATOR_IMPL(PoseDataJointVelocities, MotionMatchAllocator, 0) + + PoseDataJointVelocities::PoseDataJointVelocities() + : PoseData() + { + } + + PoseDataJointVelocities::~PoseDataJointVelocities() + { + Clear(); + } + + void PoseDataJointVelocities::Clear() + { + m_velocities.clear(); + m_angularVelocities.clear(); + } + + void PoseDataJointVelocities::LinkToActorInstance(const ActorInstance* actorInstance) + { + m_velocities.resize(actorInstance->GetNumNodes()); + m_angularVelocities.resize(actorInstance->GetNumNodes()); + + SetRelativeToJointIndex(actorInstance->GetActor()->GetMotionExtractionNodeIndex()); + } + + void PoseDataJointVelocities::SetRelativeToJointIndex(size_t relativeToJointIndex) + { + if (relativeToJointIndex == InvalidIndex) + { + m_relativeToJointIndex = 0; + } + else + { + m_relativeToJointIndex = relativeToJointIndex; + } + } + + void PoseDataJointVelocities::LinkToActor(const Actor* actor) + { + AZ_UNUSED(actor); + Clear(); + } + + void PoseDataJointVelocities::Reset() + { + const size_t numJoints = m_velocities.size(); + for (size_t i = 0; i < numJoints; ++i) + { + m_velocities[i] = AZ::Vector3::CreateZero(); + m_angularVelocities[i] = AZ::Vector3::CreateZero(); + } + } + + void PoseDataJointVelocities::CopyFrom(const PoseData* from) + { + AZ_Assert(from->RTTI_GetType() == azrtti_typeid(), "Cannot copy from pose data other than joint velocity pose data."); + const PoseDataJointVelocities* fromVelocityPoseData = static_cast(from); + + m_isUsed = fromVelocityPoseData->m_isUsed; + m_velocities = fromVelocityPoseData->m_velocities; + m_angularVelocities = fromVelocityPoseData->m_angularVelocities; + m_relativeToJointIndex = fromVelocityPoseData->m_relativeToJointIndex; + } + + void PoseDataJointVelocities::Blend(const Pose* destPose, float weight) + { + PoseDataJointVelocities* destPoseData = destPose->GetPoseData(); + + if (destPoseData && destPoseData->IsUsed()) + { + AZ_Assert(m_velocities.size() == destPoseData->m_velocities.size(), "Expected the same number of joints and velocities in the destination pose data."); + + if (m_isUsed) + { + // Blend while both, the destination pose as well as the current pose hold joint velocities. + for (size_t i = 0; i < m_velocities.size(); ++i) + { + m_velocities[i] = m_velocities[i].Lerp(destPoseData->m_velocities[i], weight); + m_angularVelocities[i] = m_angularVelocities[i].Lerp(destPoseData->m_angularVelocities[i], weight); + } + } + else + { + // The destination pose data is used while the current one is not. Just copy over the velocities from the destination. + m_velocities = destPoseData->m_velocities; + m_angularVelocities = destPoseData->m_angularVelocities; + } + } + else + { + // Destination pose either doesn't contain velocity pose data or it is unused. + // Don't do anything and keep the current velocities. + } + } + + void PoseDataJointVelocities::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color) const + { + AZ_Assert(m_pose->GetNumTransforms() == m_velocities.size(), "Expected a joint velocity for each joint in the pose."); + + const Pose* pose = m_pose; + for (size_t i = 0; i < m_velocities.size(); ++i) + { + const size_t jointIndex = i; + + // draw linear velocity + { + const Transform jointModelTM = pose->GetModelSpaceTransform(jointIndex); + const Transform relativeToWorldTM = pose->GetWorldSpaceTransform(m_relativeToJointIndex); + const AZ::Vector3 jointPosition = relativeToWorldTM.TransformPoint(jointModelTM.m_position); + + const AZ::Vector3& velocity = m_velocities[i]; + + const float scale = 0.15f; + const AZ::Vector3 velocityWorldSpace = relativeToWorldTM.TransformVector(velocity * scale); + + DebugDrawVelocity(debugDisplay, jointPosition, velocityWorldSpace, color); + } + } + } + + void PoseDataJointVelocities::CalculateVelocity(MotionInstance* motionInstance, size_t relativeToJointIndex) + { + SetRelativeToJointIndex(relativeToJointIndex); + ActorInstance* actorInstance = motionInstance->GetActorInstance(); + m_velocities.resize(actorInstance->GetNumNodes()); + m_angularVelocities.resize(actorInstance->GetNumNodes()); + for (size_t i = 0; i < m_velocities.size(); ++i) + { + Feature::CalculateVelocity(i, m_relativeToJointIndex, motionInstance, m_velocities[i]); + // TODO: Angular velocity not used yet. + } + } + + void PoseDataJointVelocities::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class()->Version(1); + } + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.h b/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.h new file mode 100644 index 0000000000..68404b41ec --- /dev/null +++ b/Gems/MotionMatching/Code/Source/PoseDataJointVelocities.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + /** + * Extends a given pose with joint-relative linear and angular velocities. + **/ + class EMFX_API PoseDataJointVelocities + : public PoseData + { + public: + AZ_RTTI(PoseDataJointVelocities, "{9C082B82-7225-4550-A52C-C920CCC2482C}", PoseData) + AZ_CLASS_ALLOCATOR_DECL + + PoseDataJointVelocities(); + ~PoseDataJointVelocities(); + + void Clear(); + + void LinkToActorInstance(const ActorInstance* actorInstance) override; + void LinkToActor(const Actor* actor) override; + void Reset() override; + + void CopyFrom(const PoseData* from) override; + void Blend(const Pose* destPose, float weight) override; + + void CalculateVelocity(MotionInstance* motionInstance, size_t relativeToJointIndex); + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color) const override; + + AZStd::vector& GetVelocities() { return m_velocities; } + const AZStd::vector& GetVelocities() const { return m_velocities; } + const AZ::Vector3& GetVelocity(size_t jointIndex) { return m_velocities[jointIndex]; } + + AZStd::vector& GetAngularVelocities() { return m_angularVelocities; } + const AZStd::vector& GetAngularVelocities() const { return m_angularVelocities; } + const AZ::Vector3& GetAngularVelocity(size_t jointIndex) { return m_angularVelocities[jointIndex]; } + + static void Reflect(AZ::ReflectContext* context); + + void SetRelativeToJointIndex(size_t relativeToJointIndex); + + private: + AZStd::vector m_velocities; + AZStd::vector m_angularVelocities; + size_t m_relativeToJointIndex = InvalidIndex; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/TrajectoryHistory.cpp b/Gems/MotionMatching/Code/Source/TrajectoryHistory.cpp new file mode 100644 index 0000000000..3a5adedf48 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/TrajectoryHistory.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + TrajectoryHistory::Sample operator*(TrajectoryHistory::Sample sample, float weight) + { + return {sample.m_position * weight, sample.m_facingDirection * weight}; + } + + TrajectoryHistory::Sample operator*(float weight, TrajectoryHistory::Sample sample) + { + return {weight * sample.m_position, weight * sample.m_facingDirection}; + } + + TrajectoryHistory::Sample operator+(TrajectoryHistory::Sample lhs, const TrajectoryHistory::Sample& rhs) + { + return {lhs.m_position + rhs.m_position, lhs.m_facingDirection + rhs.m_facingDirection}; + } + + void TrajectoryHistory::Init(const Pose& pose, size_t jointIndex, const AZ::Vector3& facingAxisDir, float numSecondsToTrack) + { + AZ_Assert(numSecondsToTrack > 0.0f, "Number of seconds to track has to be greater than zero."); + Clear(); + m_jointIndex = jointIndex; + m_facingAxisDir = facingAxisDir; + m_numSecondsToTrack = numSecondsToTrack; + + // Pre-fill the history with samples from the current joint position. + PrefillSamples(pose, /*timeDelta=*/1.0f / 60.0f); + } + + void TrajectoryHistory::AddSample(const Pose& pose) + { + Sample sample; + const Transform worldSpaceTransform = pose.GetWorldSpaceTransform(m_jointIndex); + sample.m_position = worldSpaceTransform.m_position; + sample.m_facingDirection = worldSpaceTransform.TransformVector(m_facingAxisDir).GetNormalizedSafe(); + + // The new key will be added at the end of the keytrack. + m_keytrack.AddKey(m_currentTime, sample); + + while (m_keytrack.GetNumKeys() > 2 && + ((m_keytrack.GetKey(m_keytrack.GetNumKeys() - 2)->GetTime() - m_keytrack.GetFirstTime()) > m_numSecondsToTrack)) + { + m_keytrack.RemoveKey(0); // Remove first (oldest) key + } + } + + void TrajectoryHistory::PrefillSamples(const Pose& pose, float timeDelta) + { + const size_t numKeyframes = aznumeric_caster<>(m_numSecondsToTrack / timeDelta); + for (size_t i = 0; i < numKeyframes; ++i) + { + AddSample(pose); + Update(timeDelta); + } + } + + void TrajectoryHistory::Clear() + { + m_jointIndex = 0; + m_currentTime = 0.0f; + m_keytrack.ClearKeys(); + } + + void TrajectoryHistory::Update(float timeDelta) + { + m_currentTime += timeDelta; + } + + TrajectoryHistory::Sample TrajectoryHistory::Evaluate(float time) const + { + if (m_keytrack.GetNumKeys() == 0) + { + return {}; + } + + return m_keytrack.GetValueAtTime(m_keytrack.GetLastTime() - time); + } + + TrajectoryHistory::Sample TrajectoryHistory::EvaluateNormalized(float normalizedTime) const + { + const float firstTime = m_keytrack.GetFirstTime(); + const float lastTime = m_keytrack.GetLastTime(); + const float range = lastTime - firstTime; + + const float time = (1.0f - normalizedTime) * range + firstTime; + return m_keytrack.GetValueAtTime(time); + } + + void TrajectoryHistory::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color, float timeStart) const + { + const size_t numKeyframes = m_keytrack.GetNumKeys(); + if (numKeyframes == 0) + { + return; + } + + // Clip some of the newest samples. + const float adjustedLastTime = m_keytrack.GetLastTime() - timeStart; + size_t adjustedLastKey = m_keytrack.FindKeyNumber(adjustedLastTime); + if (adjustedLastKey == InvalidIndex) + { + adjustedLastKey = m_keytrack.GetNumKeys() - 1; + } + const float firstTime = m_keytrack.GetFirstTime(); + const float range = adjustedLastTime - firstTime; + + debugDisplay.DepthTestOff(); + + for (size_t i = 0; i < adjustedLastKey; ++i) + { + const float time = m_keytrack.GetKey(i)->GetTime(); + const float normalized = (time - firstTime) / range; + if (normalized < 0.3f) + { + continue; + } + + // Decrease size and fade out alpha the older the sample is. + AZ::Color finalColor = color; + finalColor.SetA(finalColor.GetA() * 0.6f * normalized); + const float markerSize = m_debugMarkerSize * 0.7f * normalized; + + const Sample currentSample = m_keytrack.GetKey(i)->GetValue(); + debugDisplay.SetColor(finalColor); + debugDisplay.DrawBall(currentSample.m_position, markerSize, /*drawShaded=*/false); + + const float facingDirectionLength = m_debugMarkerSize * 10.0f * normalized; + debugDisplay.DrawLine(currentSample.m_position, currentSample.m_position + currentSample.m_facingDirection * facingDirectionLength); + } + } + + void TrajectoryHistory::DebugDrawSampled(AzFramework::DebugDisplayRequests& debugDisplay, + size_t numSamples, + const AZ::Color& color) const + { + debugDisplay.DepthTestOff(); + debugDisplay.SetColor(color); + + Sample lastSample = EvaluateNormalized(0.0f); + for (size_t i = 0; i < numSamples; ++i) + { + const float sampleTime = i / static_cast(numSamples - 1); + const Sample currentSample = EvaluateNormalized(sampleTime); + if (i > 0) + { + debugDisplay.DrawLine(lastSample.m_position, currentSample.m_position); + } + + debugDisplay.DrawBall(currentSample.m_position, m_debugMarkerSize, /*drawShaded=*/false); + + lastSample = currentSample; + } + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/TrajectoryHistory.h b/Gems/MotionMatching/Code/Source/TrajectoryHistory.h new file mode 100644 index 0000000000..833125d27f --- /dev/null +++ b/Gems/MotionMatching/Code/Source/TrajectoryHistory.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include + +#include +#include + +namespace EMotionFX::MotionMatching +{ + //! Used to store the trajectory history for the root motion (motion extraction node). + //! The trajectory history is independent of the trajectory feature and captures a sample with every engine tick. + //! The recorded history needs to record and track at least the time the trajectory feature/query requires. + class EMFX_API TrajectoryHistory + { + public: + void Init(const Pose& pose, size_t jointIndex, const AZ::Vector3& facingAxisDir, float numSecondsToTrack); + void Clear(); + + void Update(float timeDelta); + void AddSample(const Pose& pose); + + struct EMFX_API Sample + { + AZ::Vector3 m_position = AZ::Vector3::CreateZero(); + AZ::Vector3 m_facingDirection = AZ::Vector3::CreateZero(); + }; + + //! time in range [0, m_numSecondsToTrack] + Sample Evaluate(float time) const; + + //! time in range [0, 1] where 0 is the current character position and 1 the oldest keyframe in the trajectory history + Sample EvaluateNormalized(float normalizedTime) const; + + float GetNumSecondsToTrack() const { return m_numSecondsToTrack; } + float GetCurrentTime() const { return m_currentTime; } + size_t GetJointIndex() const { return m_jointIndex; } + + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color, float timeStart = 0.0f) const; + void DebugDrawSampled(AzFramework::DebugDisplayRequests& debugDisplay, size_t numSamples, const AZ::Color& color) const; + + private: + void PrefillSamples(const Pose& pose, float timeDelta); + + KeyTrackLinearDynamic m_keytrack; + float m_numSecondsToTrack = 0.0f; + size_t m_jointIndex = 0; + float m_currentTime = 0.0f; + AZ::Vector3 m_facingAxisDir; //! Facing direction of the character asset. (e.g. 0,1,0 when it is looking towards Y-axis) + + static constexpr float m_debugMarkerSize = 0.02f; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp b/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp new file mode 100644 index 0000000000..803623a1af --- /dev/null +++ b/Gems/MotionMatching/Code/Source/TrajectoryQuery.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + AZ::Vector3 SampleFunction(TrajectoryQuery::EMode mode, 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; + } + } + } + + void TrajectoryQuery::Update(const ActorInstance* actorInstance, + const FeatureTrajectory* trajectoryFeature, + const TrajectoryHistory& trajectoryHistory, + EMode mode, + [[maybe_unused]] AZ::Vector3 targetPos, + [[maybe_unused]] AZ::Vector3 targetFacingDir, + float timeDelta, + float pathRadius, + float pathSpeed) + { + // Build the future trajectory control points. + const size_t numFutureSamples = trajectoryFeature->GetNumFutureSamples(); + m_futureControlPoints.resize(numFutureSamples); + + if (mode == MODE_TARGETDRIVEN) + { + const AZ::Vector3 curPos = actorInstance->GetWorldSpaceTransform().m_position; + if (curPos.IsClose(targetPos, 0.1f)) + { + for (size_t i = 0; i < numFutureSamples; ++i) + { + m_futureControlPoints[i].m_position = curPos; + } + } + else + { + // NOTE: Improve it by using a curve to the target. + for (size_t i = 0; i < numFutureSamples; ++i) + { + const float sampleTime = static_cast(i) / (numFutureSamples - 1); + m_futureControlPoints[i].m_position = curPos.Lerp(targetPos, sampleTime); + } + } + } + else + { + static float phase = 0.0f; + phase += timeDelta * pathSpeed; + AZ::Vector3 base = SampleFunction(mode, 0.0f, pathRadius, phase); + for (size_t i = 0; i < numFutureSamples; ++i) + { + const float offset = i * 0.1f; + const AZ::Vector3 curSample = SampleFunction(mode, offset, pathRadius, phase); + 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 dir = deltaSample - curSample; + m_futureControlPoints[i].m_facingDirection = dir.GetNormalizedSafe(); + } + } + + // Build the past trajectory control points. + const size_t numPastSamples = trajectoryFeature->GetNumPastSamples(); + m_pastControlPoints.resize(numPastSamples); + const float pastTimeRange = trajectoryFeature->GetPastTimeRange(); + + for (size_t i = 0; i < numPastSamples; ++i) + { + const float sampleTimeNormalized = i / static_cast(numPastSamples - 1); + const TrajectoryHistory::Sample sample = trajectoryHistory.Evaluate(sampleTimeNormalized * pastTimeRange); + m_pastControlPoints[i] = { sample.m_position, sample.m_facingDirection }; + } + } + + void TrajectoryQuery::DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color) const + { + DebugDrawControlPoints(debugDisplay, m_pastControlPoints, color); + DebugDrawControlPoints(debugDisplay, m_futureControlPoints, color); + } + + void TrajectoryQuery::DebugDrawControlPoints(AzFramework::DebugDisplayRequests& debugDisplay, + const AZStd::vector& controlPoints, + const AZ::Color& color) + { + const float markerSize = 0.02f; + + const size_t numControlPoints = controlPoints.size(); + if (numControlPoints > 1) + { + debugDisplay.DepthTestOff(); + debugDisplay.SetColor(color); + + for (size_t i = 0; i < numControlPoints - 1; ++i) + { + const ControlPoint& current = controlPoints[i]; + const AZ::Vector3& posA = current.m_position; + const AZ::Vector3& posB = controlPoints[i + 1].m_position; + const AZ::Vector3 diff = posB - posA; + + debugDisplay.DrawSolidCylinder(/*center=*/(posB + posA) * 0.5f, + /*direction=*/diff.GetNormalizedSafe(), + /*radius=*/0.0025f, + /*height=*/diff.GetLength(), + /*drawShaded=*/false); + + FeatureTrajectory::DebugDrawFacingDirection(debugDisplay, current.m_position, current.m_facingDirection); + } + + for (const ControlPoint& controlPoint : controlPoints) + { + debugDisplay.DrawBall(controlPoint.m_position, markerSize, /*drawShaded=*/false); + FeatureTrajectory::DebugDrawFacingDirection(debugDisplay, controlPoint.m_position, controlPoint.m_facingDirection); + } + } + } +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Source/TrajectoryQuery.h b/Gems/MotionMatching/Code/Source/TrajectoryQuery.h new file mode 100644 index 0000000000..55d9ecf797 --- /dev/null +++ b/Gems/MotionMatching/Code/Source/TrajectoryQuery.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +#include + +#include + +#include + +namespace EMotionFX::MotionMatching +{ + class FeatureTrajectory; + + //! Builds the input trajectory query data for the motion matching algorithm. + //! Reads the number of past and future samples and the time ranges from the trajectory feature, + //! constructs the future trajectory based on the target and the past trajectory based on the trajectory history. + class EMFX_API TrajectoryQuery + { + public: + struct ControlPoint + { + AZ::Vector3 m_position; + AZ::Vector3 m_facingDirection; + }; + + enum EMode : AZ::u8 + { + MODE_TARGETDRIVEN = 0, + MODE_ONE = 1, + MODE_TWO = 2, + MODE_THREE = 3, + MODE_FOUR = 4 + }; + + void Update(const ActorInstance* actorInstance, + const FeatureTrajectory* trajectoryFeature, + const TrajectoryHistory& trajectoryHistory, + EMode mode, + AZ::Vector3 targetPos, + AZ::Vector3 targetFacingDir, + float timeDelta, + float pathRadius, + float pathSpeed); + + void DebugDraw(AzFramework::DebugDisplayRequests& debugDisplay, const AZ::Color& color) const; + + const AZStd::vector& GetPastControlPoints() const { return m_pastControlPoints; } + const AZStd::vector& GetFutureControlPoints() const { return m_futureControlPoints; } + + private: + static void DebugDrawControlPoints(AzFramework::DebugDisplayRequests& debugDisplay, + const AZStd::vector& controlPoints, + const AZ::Color& color); + + AZStd::vector m_pastControlPoints; + AZStd::vector m_futureControlPoints; + }; +} // namespace EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Tests/FeatureMatrixTests.cpp b/Gems/MotionMatching/Code/Tests/FeatureMatrixTests.cpp new file mode 100644 index 0000000000..cfbb888580 --- /dev/null +++ b/Gems/MotionMatching/Code/Tests/FeatureMatrixTests.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +namespace EMotionFX::MotionMatching +{ + class FeatureMatrixFixture + : public Fixture + { + public: + void SetUp() override + { + Fixture::SetUp(); + + // Construct 3x3 matrix: + // 1 2 3 + // 4 5 6 + // 7 8 9 + m_featureMatrix.resize(3, 3); + + float counter = 1.0f; + for (size_t row = 0; row < 3; ++row) + { + for (size_t column = 0; column < 3; ++column) + { + m_featureMatrix(row, column) = counter; + counter++; + } + } + } + + FeatureMatrix m_featureMatrix; + }; + + TEST_F(FeatureMatrixFixture, AccessOperators) + { + EXPECT_FLOAT_EQ(m_featureMatrix(1, 1), 5.0f); + EXPECT_FLOAT_EQ(m_featureMatrix(0, 2), 3.0f); + EXPECT_FLOAT_EQ(m_featureMatrix.coeff(2, 1), 8.0f); + EXPECT_FLOAT_EQ(m_featureMatrix.coeff(1, 2), 6.0f); + } + + TEST_F(FeatureMatrixFixture, SetValue) + { + m_featureMatrix(1, 1) = 100.0f; + EXPECT_FLOAT_EQ(m_featureMatrix(1, 1), 100.0f); + } + + TEST_F(FeatureMatrixFixture, Size) + { + EXPECT_EQ(m_featureMatrix.size(), 9); + EXPECT_EQ(m_featureMatrix.rows(), 3); + EXPECT_EQ(m_featureMatrix.cols(), 3); + } +} // EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Tests/FeatureSchemaTests.cpp b/Gems/MotionMatching/Code/Tests/FeatureSchemaTests.cpp new file mode 100644 index 0000000000..306e42c5cc --- /dev/null +++ b/Gems/MotionMatching/Code/Tests/FeatureSchemaTests.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + class FeatureSchemaFixture + : public Fixture + { + public: + void SetUp() override + { + Fixture::SetUp(); + m_featureSchema = AZStd::make_unique(); + DefaultFeatureSchema(*m_featureSchema.get(), {}); + } + + void TearDown() override + { + Fixture::TearDown(); + m_featureSchema.reset(); + } + + AZStd::unique_ptr m_featureSchema; + }; + + TEST_F(FeatureSchemaFixture, AddFeature) + { + m_featureSchema->AddFeature(aznew FeaturePosition()); + m_featureSchema->AddFeature(aznew FeatureVelocity()); + m_featureSchema->AddFeature(aznew FeatureTrajectory()); + EXPECT_EQ(m_featureSchema->GetNumFeatures(), 9); + } + + TEST_F(FeatureSchemaFixture, Clear) + { + m_featureSchema->Clear(); + EXPECT_EQ(m_featureSchema->GetNumFeatures(), 0); + } + + TEST_F(FeatureSchemaFixture, GetNumFeatures) + { + EXPECT_EQ(m_featureSchema->GetNumFeatures(), 6); + } + + TEST_F(FeatureSchemaFixture, GetFeature) + { + EXPECT_EQ(m_featureSchema->GetFeature(1)->RTTI_GetType(), azrtti_typeid()); + EXPECT_STREQ(m_featureSchema->GetFeature(3)->GetName().c_str(), "Left Foot Velocity"); + } + + TEST_F(FeatureSchemaFixture, GetFeatures) + { + int counter = 0; + for (const Feature* feature : m_featureSchema->GetFeatures()) + { + AZ_UNUSED(feature); + counter++; + } + EXPECT_EQ(counter, 6); + } + + TEST_F(FeatureSchemaFixture, FindFeatureById) + { + const Feature* feature = m_featureSchema->GetFeature(1); + const AZ::TypeId id = feature->GetId(); + const Feature* result = m_featureSchema->FindFeatureById(id); + EXPECT_EQ(result, feature); + } +} // EMotionFX::MotionMatching diff --git a/Gems/MotionMatching/Code/Tests/Fixture.h b/Gems/MotionMatching/Code/Tests/Fixture.h new file mode 100644 index 0000000000..1edadadae5 --- /dev/null +++ b/Gems/MotionMatching/Code/Tests/Fixture.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace EMotionFX::MotionMatching +{ + using Fixture = ComponentFixture< + AZ::MemoryComponent, + AZ::AssetManagerComponent, + AZ::JobManagerComponent, + AZ::StreamerComponent, + EMotionFX::Integration::SystemComponent, + MotionMatchingSystemComponent + >; +} diff --git a/Gems/MotionMatching/Code/Tests/MotionMatchingEditorTest.cpp b/Gems/MotionMatching/Code/Tests/MotionMatchingEditorTest.cpp new file mode 100644 index 0000000000..40217ff9bc --- /dev/null +++ b/Gems/MotionMatching/Code/Tests/MotionMatchingEditorTest.cpp @@ -0,0 +1,11 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/MotionMatching/Code/Tests/MotionMatchingTest.cpp b/Gems/MotionMatching/Code/Tests/MotionMatchingTest.cpp new file mode 100644 index 0000000000..40217ff9bc --- /dev/null +++ b/Gems/MotionMatching/Code/Tests/MotionMatchingTest.cpp @@ -0,0 +1,11 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/MotionMatching/Code/motionmatching_editor_files.cmake b/Gems/MotionMatching/Code/motionmatching_editor_files.cmake new file mode 100644 index 0000000000..e18de13f3c --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_editor_files.cmake @@ -0,0 +1,12 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/MotionMatchingEditorSystemComponent.cpp + Source/MotionMatchingEditorSystemComponent.h +) diff --git a/Gems/MotionMatching/Code/motionmatching_editor_shared_files.cmake b/Gems/MotionMatching/Code/motionmatching_editor_shared_files.cmake new file mode 100644 index 0000000000..6c797254dc --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_editor_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/MotionMatchingEditorModule.cpp +) diff --git a/Gems/MotionMatching/Code/motionmatching_editor_tests_files.cmake b/Gems/MotionMatching/Code/motionmatching_editor_tests_files.cmake new file mode 100644 index 0000000000..cf91b5c3b5 --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_editor_tests_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Tests/MotionMatchingEditorTest.cpp +) diff --git a/Gems/MotionMatching/Code/motionmatching_files.cmake b/Gems/MotionMatching/Code/motionmatching_files.cmake new file mode 100644 index 0000000000..3a414467a4 --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_files.cmake @@ -0,0 +1,52 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Include/MotionMatching/MotionMatchingBus.h + Source/MotionMatchingModuleInterface.h + Source/MotionMatchingSystemComponent.cpp + Source/MotionMatchingSystemComponent.h + Source/Allocators.h + Source/BlendTreeMotionMatchNode.cpp + Source/BlendTreeMotionMatchNode.h + Source/EventData.cpp + Source/EventData.h + Source/Frame.cpp + Source/Frame.h + Source/Feature.cpp + Source/Feature.h + Source/FeatureMatrix.cpp + Source/FeatureMatrix.h + Source/FeaturePosition.cpp + Source/FeaturePosition.h + Source/FeatureSchema.cpp + Source/FeatureSchema.h + Source/FeatureSchemaDefault.cpp + Source/FeatureSchemaDefault.h + Source/FeatureTrajectory.h + Source/FeatureTrajectory.cpp + Source/FeatureVelocity.cpp + Source/FeatureVelocity.h + Source/PoseDataJointVelocities.cpp + Source/PoseDataJointVelocities.h + Source/TrajectoryHistory.cpp + Source/TrajectoryHistory.h + Source/TrajectoryQuery.cpp + Source/TrajectoryQuery.h + Source/FrameDatabase.cpp + Source/FrameDatabase.h + Source/ImGuiMonitor.cpp + Source/ImGuiMonitor.h + Source/ImGuiMonitorBus.h + Source/KdTree.cpp + Source/KdTree.h + Source/MotionMatchingData.cpp + Source/MotionMatchingData.h + Source/MotionMatchingInstance.cpp + Source/MotionMatchingInstance.h +) diff --git a/Gems/MotionMatching/Code/motionmatching_shared_files.cmake b/Gems/MotionMatching/Code/motionmatching_shared_files.cmake new file mode 100644 index 0000000000..ac0375129e --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/MotionMatchingModule.cpp +) diff --git a/Gems/MotionMatching/Code/motionmatching_tests_files.cmake b/Gems/MotionMatching/Code/motionmatching_tests_files.cmake new file mode 100644 index 0000000000..e9d72ce9f9 --- /dev/null +++ b/Gems/MotionMatching/Code/motionmatching_tests_files.cmake @@ -0,0 +1,14 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Tests/Fixture.h + Tests/FeatureMatrixTests.cpp + Tests/FeatureSchemaTests.cpp + Tests/MotionMatchingTest.cpp +) \ No newline at end of file diff --git a/Gems/MotionMatching/Docs/Diagrams/ArchitectureDiagram.drawio b/Gems/MotionMatching/Docs/Diagrams/ArchitectureDiagram.drawio new file mode 100644 index 0000000000..e986ab9682 --- /dev/null +++ b/Gems/MotionMatching/Docs/Diagrams/ArchitectureDiagram.drawio @@ -0,0 +1 @@ +7Vxbc5s6EP41nmkfnDEIA36M7aTNOcmctElvTx0FZFstIFfIid1fX0mIq3BCXOPLHPchNasLYi/frpYVHTAKl+8onM9uiI+Cjtnzlx0w7pim2bcc/p+grBSlZyjKlGI/oRk54Q7/RorYU9QF9lFc6sgICRiel4keiSLksRINUkqeyt0mJCjfdQ6nSCPceTDQqV+wz2YJ1e33cvp7hKez9M5GT7WEMO2sCPEM+uSpQAIXHTCihLDkV7gcoUBwL+VLMu5yTWu2MIoi1mTApyX9Ti+6lx/Il6+T7q+P6ObTY9dOZnmEwUI9cMe0Az7fcC6WzFaKD/avhVjnMIR0iqMOOOetvfmS/+VE+bSC3mVknrRZhTaGlqwLAzxV4zy+YETzOfmvqfpf3hkXCDDkEw4D/epKzDGBHsrI5SGlGTlTcPUuD1SjpIRLBNmCosLoh2pfTptXaTMqWJZqcPp0xvoH3YjFAZqwAo/1uUXfqwizN+LPHWIMR9P4bTLkkXBtqGGAGHOxZBR6TD38Z6ER8ZsRiYT4aobXcaC9BxrBwFsEkKERiVnjVZml9ZiPiDLMTfs80cWx1Nah0sxxso4h4b0mgTTUCeYGBIYTfjcFTIapri9hiAOBae9R8IjErEIBWBiITtm9i8ap7FWsAS0LJGWs7xAJEaMr3kW1dq2BQg6FnV03xZanHIkyeJkVUSjtCBX6TbPZc4DgPxRGvAIvHA0vlL7ckhgzTCKN6VtmSt+pMGVg60wxjBqm9NviibuOJ59RQDzMVm3zxAL9Q+PJYB1P7in8wf00oa1zBfQOT1MG2kMjn4cb6pJQNiNTEsHgIqcOKVlEPhLTCtzM+1wTAV+SXT84yq8URMEFI2UsQkvMvorhZ3119a3QMl6qmeXFSl3EDFJ2LuInTohIhFLapcTEZEDkV3pwSqF9rRBjsqAeeoZPWaTHPQRiz3QEIOkouPisUlDEnQd+LAd1W5dvpqWa2t9ARvFSk378hMMASt5JL6NajIJbUtESGNZ7L2+GA/8arshC8ImLyPuZXg1nhOLffFqYaoKUoNIT0y71uBMjleAoElHMbSo4o0K6gctSx2sYM0XwSBDAeYwfssdInPyQMEbCl/TiFcadub3UuG3D1Y3brjHuQVvGbfZ14VMYopPQtyV0p4rozqAG0XcqdNc5FkTnfKerwiBx+a3Ylg+TVwfiCewj9QT6xlqCwRgy+ADjEyi0BwoH4An0XVIe9r7HcW30e1KADRUgC+FTBTCt/p4VYAD26RXS3xLdz5zURbzkFkpOIfcRh+kW3IZuwTUOyy3ouYIcGT4s0AkXtocLfffwcKGnifdAo8U9m/fgSKM+Pe31r39P0Snc25ZVH96233VPNt3EpkHvOG06Xbee07vzZiiEJ9PelsO27IMzbud/lbDfNEX0F6AAmsbxvW2Dghp6SzBfdGE3WX2/aIKKciVrVeMq+pUt5C/wBmh4c0PEm9UbyDgwRFORQjqBzrZAB1iVd6eWue+cMtCjSB2FclMmcxRV8CNKC6lkvUIJGnwMQxL59zNRkVGSo2HVQcaLaFcPVr1nwWod0BjtAU3T6MPeDc4AqwIzRkWbkifSUKYGsPpVr1kt/mgZsCw9QNLUVdo3TIy5V1bWSvlNPTgF8AEFQw4sU+lcRyQgVM4MJvJfp1ylU68rzxqbqh9Ua+xkG43XJCKTEbsJSy39VfMmXKcJnh0x23fK9b7+jreunK4iB1GrVs/+tT45xL6fBJMVGc6FGcvH6g87/bGYi8ePqupwa0VWwK5Hp2LhWY1LNNtyiXYDjCm4xIeAiBik6AhtLdrd0/vU1+4fNvd5TZNoTV1eXUD0t+G2qdU4VouvmjtC8NJULTtC2zwp6auVtOmLnL0qqVZfWQ2xGispqG4vneo2om0l1beXJyV9SUmdI1BSTbOOWUmtVynpvnfAG+5mN9k5b67DjSup0jrG1lNtTnUPbFfjx8011t61xtZtDI5mE5za27Ftgu2642vHswneHtt3ynVHj3NvSSxLG/8R9qWOvWAUa8I4pak325FrexsDqDmKoYRTsyVv79WYHkheJC8qLr+Kc3ngXCjFSQPa0gDT1XMyu9UAV0/KlN9UXUWc/ZF3UoL23lY1rmkzWzsS6tZlorNDyG87IvAEyi0K8qe5Dxl6MwkIZHrrfws2X9QNu8SRP0QxS5VLltPX9Bujh8V0TOGTaKtPxMaMkp8odeTqVbg47FshNYrK4jn0+HKuZZ+xlVM+KuYLUuFQ8Qz7vtyuUMIgW5vZHXJxjcTOoM8XPuLXRn4t075zvhkZEW5hFGKpUIjr4xOKWa2qPW/ALytg+vkGu5m2uVZbytYgxxZgVf0gpJx+tMHYSMRZGl7J9F5WcHQNTe5AlzuokbGMJbOj0nnkqWf19yJeoybPXy/ftsSrBxVdfhl+/yVqo0ufJigYftIlQpwhMZO4oLdy4+ON4usBssNV5KOl1okLKvJvKZnyieJ7LM7uZT3Ozs5OeLJ9hWtY4ZXa/fZLsi1Nqns7qrHZSQ3nsI9qpOHBy3knlWU9kLrPQYOMzgHlIHeYd99cF5ym5X5piqH1HKSeOOxVfNvx5CAH28mG7SkHmdrbseUgB/phz024vq8c5PbYvlOuZ1842rHfPsrTku7+YHeNkPll/uG7BP/y7weCiz8= \ No newline at end of file diff --git a/Gems/MotionMatching/Docs/Diagrams/FeatureSchema.drawio b/Gems/MotionMatching/Docs/Diagrams/FeatureSchema.drawio new file mode 100644 index 0000000000..b2b17a1965 --- /dev/null +++ b/Gems/MotionMatching/Docs/Diagrams/FeatureSchema.drawio @@ -0,0 +1 @@ +5Z1dd6o4FIZ/jZd2QRJULmt72llrzqyeaS965hIlKlMEC7Ffv34SDCiJzhzt3nY0V+oGX0iel4+9ibFDr+Zvt0W0mP2RxzztEC9+69DrDiF+L+zLFxV5X0VCXwemRRLrldaBh+SD66Cno8sk5mVrRZHnqUgW7eA4zzI+Fq1YVBT5a3u1SZ62t7qIptwKPIyj1I4+JrGYraKDwFvHf+PJdFZv2ff0knlUr6wD5SyK89eNEP3WoVdFnovVu/nbFU9V59X9svrezY6lzY4VPBO/8oWP7OPHcDF4/pmO7srrx+w2e/y9S1YqL1G61A3ukF4q9YYjtcviXfdD73mp9nM4yTPRLStKl3IFP1hI0sP1cvluql5veCSWBVer1IJyz0b14mYjxae2cq86T/VBKV9uJku9SVFEf0sv5MW77vlmA0RCWKi34+VIvgxfZ4ngD4torGKv0sEyNhPzVH7y1e7lyyzm8fdRE4jGT9NCRe+WIk0yruNxVDzdSZlEKJt7F17QDpIqqtZcteqaqVYmaXqVp3lR7RoNY8bjWLdeHwh+ULXWBK3Zv/BC8LeNkAZ/y/M5F6r1Xr3UC1df0Udhbd/XtaUp1bHZhp3ZQAcjfRhNG+m10+QbbbY9jEfRjUcRjfedT0R1Nqn898LTvEJ/Sm4bTXqTCY7bgvA/3eazo7qNobuNHM1ti7xMRJJnp+S2ySSKI6RzG2P/N7f1drpNNf9XrECYtILttiKay3uhtdFWcrXXDDvI7hNt5KUo8ideY8lyhbhFSoeiNJlmykgSCpfxoYKRyHuiS71gnsRxustjxcpYyhXVJxFVbqXX3VAFmtsgDwZ/V95kXpCgZQEysD3ABuQisF1AsEzQxzJBc8oZyqvO0+naoWUGH+k+JwgsI4TUP6IN/MEWHxhoeBZfqlRl3eUblHb2Co9bmYvdJxttDrac/+pYwVOJ5KWd72zrB72FH3lS+fetLWOcfWuBMl8WY66/s5mdGDJN3rRDR0TFlAtLp2LStPlwTDV/VzD1gDCZOtiYfLcw9YEwmTrYmLaVFc4Yk3kneSgmUwcb07Yk/IwxhUCYTB1sTNuy1zPG1HT3ZzlZQtigAsdA+VCgTCFsUNsy9HMGZaY3B4MyhbBBbcuizxkUhQJlCmGDcizNbSqKnwZlCmGDCh0DBVWQsISQQdUHsDOgoEoSlhAyKOZYFuX3QihQ4XFBuZZH9aFAmULYoFzLowZQoEwhbFCu5VEhFChTCBuUY3kU8YBAWULYoBzLo+SRAATKFMIG5VgeRQgUKFMIGVTdDmdAUShQphA2KMee7RIGBcoUwgZFHAMVQIEyhbBBOVaZIFCVCUsIG5RjlQkCVZmwhLBBOVaZABsycewxE4FjlQkKNWbCEsIG5VhlgkKNmbCEsEE5VpmgUGMmLCFsUI5VJijUmAlLCBlU/ZzSGVBQYyYsIWxQjlUmKNSYCUsIGxRxDBTUmAlLCBuUY5UJCvVDDksIG5RjlQkK9VMOSwgblGOVCQpVmbCEsEE5VplgUJUJSwgZVN+xPIpBVSYsIWRQ9ZnWGVBQlQlLCBuUY3kUg6pMWELYoBzLoxjzgUCZQtigHMujWAAFyhTCBuVYHsV6UKBMIWxQjuVRrA8FyhTCBuVaHjWAAmUKYYNy7AkvC6FAmULYoBx7wht4QKAsIWxQjlUmAh8KlCmEDKouLToDylQ4GJQphA3KscpEQKFAmULYoPa7mRinUVkm42qyy6gQdviEEFpzWx46nIIx79+FsBnud59xTgytuYoPZRiEX8xwv1uQc2JozW5+KMP+8Y7D6C6cdp+v/yp/PvB7ckuHD/d/drdd82AnRQ8w//uhAnvSc/CP4hHBmoM/3DGf7lFmRd9qN4JuN3Y8u53kJPzxZDLq4ditb+XWX223/Z6MnNMFymJx6AVqUKdFX3SB2u+ZyTkhtM7ehw+dZlgM5cf1n1etVl//BRj99g8= \ No newline at end of file diff --git a/Gems/MotionMatching/Docs/Images/ArchitectureDiagram.png b/Gems/MotionMatching/Docs/Images/ArchitectureDiagram.png new file mode 100644 index 0000000000..5eb0507284 --- /dev/null +++ b/Gems/MotionMatching/Docs/Images/ArchitectureDiagram.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a4ae0e6c7e54ab84acd0582c7a5375ce96c73c4765cd69b542bc97609a3a25f +size 316431 diff --git a/Gems/MotionMatching/Docs/Images/FeatureSchema.png b/Gems/MotionMatching/Docs/Images/FeatureSchema.png new file mode 100644 index 0000000000..51fb8a9db4 --- /dev/null +++ b/Gems/MotionMatching/Docs/Images/FeatureSchema.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e14441badf5f7c85d538aa09b1d1ae2af48c3263221930a058c6fd48cc60e3 +size 193077 diff --git a/Gems/MotionMatching/JupyterNotebooks/FeatureAnalysis.ipynb b/Gems/MotionMatching/JupyterNotebooks/FeatureAnalysis.ipynb new file mode 100644 index 0000000000..44059714f9 --- /dev/null +++ b/Gems/MotionMatching/JupyterNotebooks/FeatureAnalysis.ipynb @@ -0,0 +1,352 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d57026d3", + "metadata": {}, + "source": [ + "# Settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26e1d687", + "metadata": {}, + "outputs": [], + "source": [ + "featureMatrixFilePath = 'E:/MotionMatchingFeatureMatrix.csv'" + ] + }, + { + "cell_type": "markdown", + "id": "a45e3d25", + "metadata": {}, + "source": [ + "# Load feature matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25a44238", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "from sklearn import preprocessing\n", + "from sklearn.decomposition import PCA\n", + "\n", + "def PrintGreen(text):\n", + " print('\\x1b[6;30;42m' + text + '\\x1b[0m')\n", + " \n", + "def PrintRed(text):\n", + " print('\\33[41m' + text + '\\x1b[0m')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bdc881e", + "metadata": {}, + "outputs": [], + "source": [ + "# Load the feature matrix from CSV\n", + "originalData = pd.read_csv(featureMatrixFilePath, na_values = 'null')\n", + "if originalData.shape[0] > 0 and originalData.shape[1] > 0:\n", + " PrintGreen(\"Loading succeeded\");\n", + "else:\n", + " PrintRed(\"Loading failed!\");\n", + "\n", + "print(\"frames = \" + str(originalData.shape[0]))\n", + "print(\"featureComponents = \" + str(originalData.shape[1]))\n", + "\n", + "# Ensure to show all columns\n", + "pd.set_option('max_columns', originalData.shape[1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e41c7caf", + "metadata": {}, + "outputs": [], + "source": [ + "originalData.head(15)" + ] + }, + { + "cell_type": "markdown", + "id": "b3bbc348", + "metadata": {}, + "source": [ + "# Data preparation\n", + "\n", + "1. Data Cleaning: We will remove unused feature components that are zeroed out for now as they are not implemented yet.\n", + "2. Feature Selection: Happened in the motion matching gem. So far we have a position, velocity and a trajectory feature.\n", + "3. Data Transformation: We will change the scale of our features by normalizing it using min-max normalization. We do not modify the distribution for now.\n", + "4. Feature Engineering / Data Augmentation: We will not derive new variables for now.\n", + "5. Dimensionality Reduction: We will not create compact projections of the data for now.\n", + "\n", + "# Data cleaning\n", + "Remove columns containing only 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a64a465", + "metadata": {}, + "outputs": [], + "source": [ + "def CleanData(data):\n", + " # Remove columns with only zeros\n", + " cleanedData = data[data.columns[(data != 0).any()]]\n", + " \n", + " if cleanedData.shape[0] != data.shape[0]:\n", + " PrintRed(\"Frame count of original and cleaned data should match!\")\n", + " \n", + " if cleanedData.shape[1] < data.shape[1]:\n", + " PrintGreen(str(data.shape[1] - cleanedData.shape[1]) + \" feature components containing only 0.0 values removed\");\n", + " \n", + " print(\"frames = \" + str(cleanedData.shape[0]))\n", + " print(\"featureComponents = \" + str(cleanedData.shape[1]))\n", + " \n", + " return cleanedData\n", + "\n", + "\n", + "cleanedData = CleanData(originalData);\n", + "frameCount = cleanedData.shape[0]\n", + "cleanedFeatureComponentCount = cleanedData.shape[1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81b759dc", + "metadata": {}, + "outputs": [], + "source": [ + "cleanedData.head(15)" + ] + }, + { + "cell_type": "markdown", + "id": "b9e9a8e2", + "metadata": {}, + "source": [ + "# Feature analysis visualizations" + ] + }, + { + "cell_type": "markdown", + "id": "643dd550", + "metadata": {}, + "source": [ + "## Histogram per feature component showing value distributions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3cbe721", + "metadata": {}, + "outputs": [], + "source": [ + "def Histogram(data):\n", + " image = data.hist(figsize = [32, 32])\n", + "\n", + " \n", + "Histogram(cleanedData)" + ] + }, + { + "cell_type": "markdown", + "id": "06cc1e64", + "metadata": {}, + "source": [ + "## Boxplot per feature component\n", + "Median in orange inside the box
\n", + "Box = Interquartile range, which means 50% of the data lies within the box
\n", + "Black line range = 99,3% of the values
\n", + "Semi-transparent outliers represent the rest 0.7%
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ff8c258", + "metadata": {}, + "outputs": [], + "source": [ + "def BoxPlot(data, featureComponentCount):\n", + " minValuePerColumn = data.min(axis=0)\n", + " maxValuePerColumn = data.max(axis=0)\n", + "\n", + " fig1, ax1 = plt.subplots(figsize=(20,20))\n", + " ax1.set_title('Feature Component Boxplot')\n", + "\n", + " # Render outliers\n", + " flierprops = dict(marker='o', markerfacecolor='gainsboro', markersize=1, linestyle='none', markeredgecolor='gainsboro', alpha=0.005)\n", + " ax1.boxplot(data, vert=False, flierprops=flierprops)\n", + "\n", + " # Create an array containing values ranging from 1 to featureComponentCount\n", + " elementNumbers = np.array([i+1 for i in range(featureComponentCount)])\n", + "\n", + " plt.yticks(elementNumbers, data.columns)\n", + " plt.show()\n", + "\n", + "\n", + "BoxPlot(cleanedData, cleanedData.shape[1])" + ] + }, + { + "cell_type": "markdown", + "id": "023ab81b", + "metadata": {}, + "source": [ + "## Feature correlation heatmap" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "290aff93", + "metadata": {}, + "outputs": [], + "source": [ + "# not used in drawing, this just prints the values\n", + "correlationMatrix = cleanedData.corr()\n", + "\n", + "# plot the correlation heatmap\n", + "plt.figure(figsize=[32, 32])\n", + "sns.heatmap(data=correlationMatrix)" + ] + }, + { + "cell_type": "markdown", + "id": "2ce964ae", + "metadata": {}, + "source": [ + "## Scatterplot using PCA\n", + "Use principal component analysis to project the multi-dimensional data down to 2D" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d77c43aa", + "metadata": {}, + "outputs": [], + "source": [ + "def ScatterPlotPCA(data):\n", + " pca = PCA(n_components=2)\n", + " pca.fit(data)\n", + " pcaData = pca.transform(data)\n", + " \n", + " pca_x = pcaData[:, 0]\n", + " pca_y = pcaData[:, 1]\n", + " plt.figure(figsize=(16, 16))\n", + " plt.scatter(pca_x, pca_y, s=2.0, alpha=0.5)\n", + "\n", + " \n", + "ScatterPlotPCA(cleanedData)" + ] + }, + { + "cell_type": "markdown", + "id": "e3c4c80a", + "metadata": {}, + "source": [ + "# Data Transformation\n", + "# Normalization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c71d0a5", + "metadata": {}, + "outputs": [], + "source": [ + "# mean normalization\n", + "# normalized_df=(df-df.mean())/df.std()\n", + "\n", + "# min-max normalization\n", + "# normalized_df=(df-df.min())/(df.max()-df.min())\n", + "\n", + "# Note: Pandas automatically applies colomn-wise function in the code above.\n", + "\n", + "# Using sklearn\n", + "x = cleanedData.values\n", + "min_max_scaler = preprocessing.MinMaxScaler(feature_range=(0, 1))\n", + "x_scaled = min_max_scaler.fit_transform(x)\n", + "\n", + "normalizedData = pd.DataFrame(data=x_scaled, columns=cleanedData.columns) # copy column names from source\n", + "\n", + "# min values per column used to normalize the data\n", + "print(\"Minimum values per feature component / column\")\n", + "print(min_max_scaler.data_min_)\n", + "print(\"\")\n", + "\n", + "# max values per column used to normalize the data\n", + "print(\"Maximum values per feature component / column\")\n", + "print(min_max_scaler.data_max_)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b5bfd21", + "metadata": {}, + "outputs": [], + "source": [ + "normalizedData.head(15)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c094066c", + "metadata": {}, + "outputs": [], + "source": [ + "Histogram(normalizedData)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b81660c2", + "metadata": {}, + "outputs": [], + "source": [ + "ScatterPlotPCA(normalizedData)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Gems/MotionMatching/README.md b/Gems/MotionMatching/README.md new file mode 100644 index 0000000000..ecad3387f2 --- /dev/null +++ b/Gems/MotionMatching/README.md @@ -0,0 +1,48 @@ +# Motion Matching + +Motion matching is a data-driven animation technique that synthesizes motions based on existing animation data and the current character and input contexts. + +# Features + +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. + +Features are responsible for each of the following: + +1. Extract the feature values for a given frame in the motion database and store them in the feature matrix. For example calculate the left foot joint linear velocity, convert it to relative-to the root joint model space for frame 134 and place the XYZ components in the feature matrix starting at column 9. +1. Extract the feature from the current input context/pose and fill the query vector with it. For example calculate the linear velocity of the left foot joint of the current character pose in relative-to the root joint model space and place the XYZ components in the feature query vector starting at position 9. +1. Calculate the cost of the feature so that the motion matching algorithm can weight it into search for the next best matching frame. An example would be calculating the squared distance between a frame in the motion matching database and the current character pose for the left foot joint. + +# Feature schema + +The feature schema is a set of features that define the criteria used in the motion matching algorithm and influences the runtime speed, memory used, and the results of the synthesized motion. It is the most influential, user-defined input to the system. + +The schema defines which features are extracted from the motion database while the actual extracted data is stored in the feature matrix. Along with the feature type, settings like the joint to extract the data from, a debug visualization color, how the residual is calculated or a custom feature is specified. + +The more features are selected by the user, the bigger the chances are that the searched and matched pose hits the expected result but the slower the algorithm will be and the more memory will be used. The key is to use crucial and independent elements that define a pose and its movement without being too strict on the wrong end. The root trajectory along with the left and right foot positions and velocities have been proven to be a good start here. + +# Feature matrix + +The feature matrix is an NxN 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. + +![Feature Schema](Docs/Images/FeatureSchema.png) + +# Trajectory History + +The trajectory history stores world space position and facing direction data of the root joint (motion extraction joint) with each game tick. The maximum recording time is adjustable but needs to be at least as long as the past trajectory window from the trajectory feature as the trajectory history is used to build the query for the past trajectory feature. + +# Motion Matching data + +Data based on a given skeleton but independent of the instance like the motion capture database, the feature schema or feature matrix is stored in here. It is just a wrapper to group the sharable data. + +# Motion Matching instance + +The instance is where everything comes together. It stores the trajectory history, the trajectory query along with the query vector, knows about the last lowest cost frame frame index and stores the time of the animation that the instance is currently playing. It is responsible for motion extraction, blending towards a new frame in the motion capture database in case the algorithm found a better matching frame and executes the actual search. + +# Architecture Diagram +![Class Diagram](Docs/Images/ArchitectureDiagram.png) \ No newline at end of file diff --git a/Gems/MotionMatching/gem.json b/Gems/MotionMatching/gem.json new file mode 100644 index 0000000000..60e8be01c0 --- /dev/null +++ b/Gems/MotionMatching/gem.json @@ -0,0 +1,21 @@ +{ + "gem_name": "MotionMatching", + "display_name": "Motion Matching", + "license": "Apache-2.0 Or MIT", + "license_url": "https://github.com/o3de/o3de/blob/development/LICENSE.txt", + "origin": "Open 3D Engine - o3de.org", + "type": "Code", + "summary": "Motion matching is a data-driven animation technique that synthesizes motions based on existing animation data and the current character and input contexts.", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "Animation", + "Tools", + "Simulation" + ], + "icon_path": "preview.png", + "requirements": "", + "dependencies": [ + "EMotionFX"] +} diff --git a/Gems/MotionMatching/preview.png b/Gems/MotionMatching/preview.png new file mode 100644 index 0000000000..0f393ac886 --- /dev/null +++ b/Gems/MotionMatching/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ac9dd09bde78f389e3725ac49d61eff109857e004840bc0bc3881739df9618d +size 2217 diff --git a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp index 6bd8f0d049..af21e7297c 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp +++ b/Gems/Prefab/PrefabBuilder/PrefabGroup/PrefabGroupBehavior.cpp @@ -166,7 +166,7 @@ namespace AZ::SceneAPI::Behaviors meshNodeFullName.append(meshNodeName.GetName()); auto meshGroup = AZStd::make_shared(); - meshGroup->SetName(meshNodeFullName.c_str()); + meshGroup->SetName(meshNodeFullName); meshGroup->GetSceneNodeSelectionList().AddSelectedNode(AZStd::move(meshNodePath)); for (const auto& meshGoupNamePair : meshTransformMap) { @@ -374,10 +374,18 @@ namespace AZ::SceneAPI::Behaviors Events::ProcessingResult PrefabGroupBehavior::ExportEventHandler::UpdateManifest( Containers::Scene& scene, ManifestAction action, - [[maybe_unused]] RequestingApplication requester) + RequestingApplication requester) { - if (action != Events::AssetImportRequest::ConstructDefault) + if (action == Events::AssetImportRequest::Update) { + // ignore constructing a default procedural prefab if some tool or script is attempting + // to update the scene manifest + return Events::ProcessingResult::Ignored; + } + else if (action == Events::AssetImportRequest::ConstructDefault && requester == RequestingApplication::Editor) + { + // ignore constructing a default procedurla prefab if the Editor's "Edit Settings..." is being used + // the user is trying to assign the source scene asset their own mesh groups return Events::ProcessingResult::Ignored; } diff --git a/engine.json b/engine.json index 5c64eed308..a476c1b769 100644 --- a/engine.json +++ b/engine.json @@ -49,6 +49,7 @@ "Gems/MessagePopup", "Gems/Metastream", "Gems/Microphone", + "Gems/MotionMatching", "Gems/Multiplayer", "Gems/MultiplayerCompression", "Gems/NvCloth", diff --git a/scripts/build/Platform/Android/gradle_windows.cmd b/scripts/build/Platform/Android/gradle_windows.cmd index bb98799444..f2d423282f 100644 --- a/scripts/build/Platform/Android/gradle_windows.cmd +++ b/scripts/build/Platform/Android/gradle_windows.cmd @@ -31,11 +31,19 @@ IF NOT EXIST %OUTPUT_DIRECTORY% ( mkdir %OUTPUT_DIRECTORY% ) -REM Jenkins reports MSB8029 when TMP/TEMP is not defined, define a dummy folder -SET TMP=%cd%/temp -SET TEMP=%cd%/temp -IF NOT EXIST %TMP% ( - mkdir %TMP% +REM Jenkins does not defined TMP +IF "%TMP%"=="" ( + IF "%WORKSPACE%"=="" ( + SET TMP=%APPDATA%\Local\Temp + SET TEMP=%APPDATA%\Local\Temp + ) ELSE ( + SET TMP=%WORKSPACE%\Temp + SET TEMP=%WORKSPACE%\Temp + REM This folder may not be created in the workspace + IF NOT EXIST "!TMP!" ( + MKDIR "!TMP!" + ) + ) ) REM Optionally sign the APK if we are generating an APK