diff --git a/EndlessVendetta/EndlessVendetta.uproject b/EndlessVendetta/EndlessVendetta.uproject index f9454b2c..afc163ca 100644 --- a/EndlessVendetta/EndlessVendetta.uproject +++ b/EndlessVendetta/EndlessVendetta.uproject @@ -33,6 +33,11 @@ "Name": "VaultIt", "Enabled": true, "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/7d19b5622d6846edb9881e6c89bce05e" + }, + { + "Name": "MixamoAnimationRetargeting", + "Enabled": true, + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/c684998124da4e2583b314dc95403a80" } ] } \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Content/Mixamo/CR_Mixamo_BasicFootIK.uasset b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Content/Mixamo/CR_Mixamo_BasicFootIK.uasset new file mode 100644 index 00000000..6f03e9af --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Content/Mixamo/CR_Mixamo_BasicFootIK.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10557c6e272eb9aa7478f1f7a66cefa4e89e5b37a658e868104574929f35f70f +size 619423 diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/MixamoAnimationRetargeting.uplugin b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/MixamoAnimationRetargeting.uplugin new file mode 100644 index 00000000..a710887b --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/MixamoAnimationRetargeting.uplugin @@ -0,0 +1,37 @@ +{ + "FileVersion": 3, + "Version": 9, + "VersionName": "2.2.5", + "FriendlyName": "Mixamo Animation Retargeting 2", + "Description": "Editor plugin for precise and automated retargeting of skeletons, skeletal meshes and animations created with and exported from Mixamo tools (Auto-Rigger, 3D Characters, 3D Animations).", + "Category": "Editor", + "CreatedBy": "UNAmedia", + "CreatedByURL": "https://www.unamedia.com", + "DocsURL": "https://www.unamedia.com/ue5-mixamo/docs/", + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/c684998124da4e2583b314dc95403a80", + "SupportURL": "https://www.unamedia.com/ue5-mixamo", + "EngineVersion": "5.1.0", + "CanContainContent": true, + "Installed": true, + "Modules": [ + { + "Name": "MixamoAnimationRetargeting", + "Type": "EditorNoCommandlet", + "LoadingPhase": "Default", + "PlatformAllowList": [ + "Win64", + "Mac" + ] + } + ], + "Plugins": [ + { + "Name": "IKRig", + "Enabled": true + }, + { + "Name": "ContentBrowserAssetDataSource", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/ButtonIcon_40x.png b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/ButtonIcon_40x.png new file mode 100644 index 00000000..04084609 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/ButtonIcon_40x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dce1006aebf4ecf885631045465e68f1608e3a09ee01c4577850e553383511e0 +size 2273 diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/Icon128.png b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/Icon128.png new file mode 100644 index 00000000..dd275d3e --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/Icon128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a50465581f45ffe8d36e28bdbb51ad6d9a342cfbc313ac4e7c20bc44bd61c825 +size 11169 diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/MixamoAnimationRetargeting.Build.cs b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/MixamoAnimationRetargeting.Build.cs new file mode 100644 index 00000000..e0a19515 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/MixamoAnimationRetargeting.Build.cs @@ -0,0 +1,73 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +using UnrealBuildTool; + +public class MixamoAnimationRetargeting : ModuleRules +{ + public MixamoAnimationRetargeting(ReadOnlyTargetRules Target) : base(Target) + { + ShortName="MAR"; + + PublicIncludePaths.AddRange( + new string[] { + //"MixamoAnimationRetargeting/Public" + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + "MixamoAnimationRetargeting/Private", + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + "Projects", + "InputCore", + //"LevelEditor", + "CoreUObject", + "Engine", + "UnrealEd", + "Slate", + "SlateCore", + "EditorStyle", + "ContentBrowser", + "ContentBrowserData", + "ContentBrowserAssetDataSource", + "RenderCore", + "IKRig", + "IKRigEditor", + "MessageLog" + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + + // Needed to avoid a deadlock of errors: file "XXX.cpp" is required + // to include file "XXX.h" as first header file; but without this + // option it's also required to include the PCH header file as first + // file... + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + } +} diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRetargeting.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRetargeting.cpp new file mode 100644 index 00000000..6b00f898 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRetargeting.cpp @@ -0,0 +1,90 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoAnimationRetargeting.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "MixamoToolkitPrivate.h" + +#include "MixamoToolkitStyle.h" +#include "MixamoToolkitCommands.h" +#include "MixamoToolkitEditorIntegration.h" + +#include "MixamoSkeletonRetargeter.h" +#include "MixamoAnimationRootMotionSolver.h" + +#include "MessageLogModule.h" + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +DEFINE_LOG_CATEGORY(LogMixamoToolkit) + + + +FMixamoAnimationRetargetingModule & FMixamoAnimationRetargetingModule::Get() +{ + static FName MixamoToolkitModuleName("MixamoAnimationRetargeting"); + return FModuleManager::Get().LoadModuleChecked(MixamoToolkitModuleName); +} + + + +void FMixamoAnimationRetargetingModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + + MixamoSkeletonRetargeter = MakeShareable(new FMixamoSkeletonRetargeter()); + MixamoAnimationRootMotionSolver = MakeShareable(new FMixamoAnimationRootMotionSolver()); + + // Register Slate style ovverides + FMixamoToolkitStyle::Initialize(); + FMixamoToolkitStyle::ReloadTextures(); + + // === Register commands. + FMixamoToolkitCommands::Register(); + + EditorIntegration = MakeShareable(new FMixamoToolkitEditorIntegration()); + EditorIntegration->Register(); + + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + MessageLogModule.RegisterLogListing(FName("LogMixamoToolkit"), LOCTEXT("MixamoRetargeting", "Mixamo Retargeting Log")); +} + + + +void FMixamoAnimationRetargetingModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + + EditorIntegration->Unregister(); + EditorIntegration.Reset(); + + FMixamoToolkitCommands::Unregister(); + FMixamoToolkitStyle::Shutdown(); + + MixamoAnimationRootMotionSolver.Reset (); + MixamoSkeletonRetargeter.Reset (); +} + + + +TSharedRef FMixamoAnimationRetargetingModule::GetMixamoSkeletonRetargeter() +{ + return MixamoSkeletonRetargeter.ToSharedRef(); +} + + + +TSharedRef FMixamoAnimationRetargetingModule::GetMixamoAnimationRootMotionSolver() +{ + return MixamoAnimationRootMotionSolver.ToSharedRef(); +} + + + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FMixamoAnimationRetargetingModule, MixamoAnimationRetargeting) \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.cpp new file mode 100644 index 00000000..7b3f569b --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.cpp @@ -0,0 +1,235 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoAnimationRootMotionSolver.h" + +#include "MixamoToolkitPrivatePCH.h" + +#include "MixamoToolkitPrivate.h" + +#include "Editor.h" +#include "SMixamoToolkitWidget.h" +#include "Misc/MessageDialog.h" + +#include "Animation/AnimSequence.h" +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" + +#include "AssetToolsModule.h" +#include "AssetRegistry/AssetRegistryModule.h" + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + +void FMixamoAnimationRootMotionSolver::LaunchProcedureFlow(USkeleton* Skeleton) +{ + checkf(Skeleton != nullptr, TEXT("A reference skeleton must be specified.")); + checkf(CanExecuteProcedure(Skeleton), TEXT("Incompatible skeleton.")); + + TSharedRef WidgetWindow = SNew(SWindow) + .Title(LOCTEXT("FMixamoAnimationRootMotionSolver_AskUserForAnimations_WindowTitle", "Select animations")) + .ClientSize(FVector2D(1000, 600)) + .SupportsMinimize(false) + .SupportsMaximize(false) + .HasCloseButton(false); + + TSharedRef RootMotionExtractionWidget = SNew(SRootMotionExtractionWidget) + .ReferenceSkeleton(Skeleton); + + WidgetWindow->SetContent(RootMotionExtractionWidget); + + GEditor->EditorAddModalWindow(WidgetWindow); + + UAnimSequence* SelectedAnimation = RootMotionExtractionWidget->GetSelectedAnimation(); + UAnimSequence* SelectedInPlaceAnimation = RootMotionExtractionWidget->GetSelectedInPlaceAnimation(); + + if (!SelectedAnimation || !SelectedInPlaceAnimation) + return; + + // check, with an heuristic, that the user has selected the right "IN PLACE" animation otherwise prompt a message box as warning + const UAnimSequence* EstimatedInPlaceAnim = EstimateInPlaceAnimation(SelectedAnimation, SelectedInPlaceAnimation); + if (EstimatedInPlaceAnim != SelectedInPlaceAnimation) + { + FText WarningText = LOCTEXT("SRootMotionExtractionWidget_InPlaceAnimWarning", "Warning: are you sure to have choose the right IN PLACE animation?"); + if (FMessageDialog::Open(EAppMsgType::YesNo, WarningText) == EAppReturnType::No) + return; + } + + static const FName NAME_AssetTools = "AssetTools"; + IAssetTools* AssetTools = &FModuleManager::GetModuleChecked(NAME_AssetTools).Get(); + + const FString ResultAnimationName = SelectedAnimation->GetName() + "_rootmotion"; + const FString PackagePath = FAssetData(SelectedAnimation).PackagePath.ToString(); + UAnimSequence* ResultAnimation = Cast(AssetTools->DuplicateAsset(ResultAnimationName, PackagePath, SelectedAnimation)); + if (!ResultAnimation) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Aborted: failed to duplicate the animation sequence."))); + return; + } + + if (ExecuteExtraction(ResultAnimation, SelectedInPlaceAnimation)) + { + ResultAnimation->bEnableRootMotion = true; + + // focus the content browser on the new animation + FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); + TArray SyncObjects; + SyncObjects.Add(ResultAnimation); + ContentBrowserModule.Get().SyncBrowserToAssets(SyncObjects); + } + else + { + FText WarningText = LOCTEXT("SRootMotionExtractionWidget_ExtractionFailedMsg", "Root motion extraction has failed, please double check the input animation sequences (ordinary and inplace). See console for additional details."); + FMessageDialog::Open(EAppMsgType::Ok, WarningText); + + ResultAnimation->MarkAsGarbage(); + } +} + + + +bool FMixamoAnimationRootMotionSolver::CanExecuteProcedure(const USkeleton* Skeleton) const +{ + // Check the asset content. + // NOTE: this will load the asset if needed. + if (!FMixamoAnimationRetargetingModule::Get().GetMixamoSkeletonRetargeter()->IsMixamoSkeleton(Skeleton)) + { + return false; + } + + // Check that the skeleton was processed with our retargeter ! + int32 RootBoneIndex = Skeleton->GetReferenceSkeleton().FindBoneIndex(TEXT("root")); + if (RootBoneIndex == INDEX_NONE) + { + return false; + } + + return true; +} + + + +bool FMixamoAnimationRootMotionSolver::ExecuteExtraction(UAnimSequence* AnimSequence, const UAnimSequence* InPlaceAnimSequence) +{ + UAnimDataModel* AnimDataModel = AnimSequence->GetDataModel(); + UAnimDataModel* InPlaceAnimDataModel = InPlaceAnimSequence->GetDataModel(); + + // take the hips bone track data from both animation sequences + const FBoneAnimationTrack* HipsBoneTrack = AnimDataModel->FindBoneTrackByName(FName("Hips")); + if (!HipsBoneTrack) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Hips bone not found in the ordinary animation sequence."))); + return false; + } + + const FBoneAnimationTrack* InPlaceHipsBoneTrack = InPlaceAnimDataModel->FindBoneTrackByName(FName("Hips")); + if (!InPlaceHipsBoneTrack) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Hips bone not found in the inplace animation sequence."))); + return false; + } + + auto& HipsTrackData = HipsBoneTrack->InternalTrackData; + auto& InPlaceHipsTrackData = InPlaceHipsBoneTrack->InternalTrackData; + + // nummber of keys should match between the two animations. + if (HipsTrackData.PosKeys.Num() != InPlaceHipsTrackData.PosKeys.Num()) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Track data keys number mismatch between ordinary and inplace animation sequences."))); + return false; + } + + // PosKeys, RotKeys and ScaleKeys should have the same size + if (FMath::Max3(HipsTrackData.PosKeys.Num(), HipsTrackData.RotKeys.Num(), HipsTrackData.ScaleKeys.Num()) + != FMath::Min3(HipsTrackData.PosKeys.Num(), HipsTrackData.RotKeys.Num(), HipsTrackData.ScaleKeys.Num())) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Invalid track key data on ordinary animation sequence, expected uniform data."))); + return false; + } + + // PosKeys, RotKeys and ScaleKeys should have the same size + if (FMath::Max3(InPlaceHipsTrackData.PosKeys.Num(), InPlaceHipsTrackData.RotKeys.Num(), InPlaceHipsTrackData.ScaleKeys.Num()) + != FMath::Min3(InPlaceHipsTrackData.PosKeys.Num(), InPlaceHipsTrackData.RotKeys.Num(), InPlaceHipsTrackData.ScaleKeys.Num())) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Invalid track key data on inplace animation sequence, expected uniform data."))); + return false; + } + + // make a new track for the root bone + // the keys num is equal to the hips keys num + FRawAnimSequenceTrack RootBoneTrack; + const int32 NumOfKeys = HipsTrackData.PosKeys.Num(); + RootBoneTrack.PosKeys.SetNum(NumOfKeys); + RootBoneTrack.RotKeys.SetNum(NumOfKeys); + RootBoneTrack.ScaleKeys.SetNum(NumOfKeys); + + // HipsBoneTrack = Root + Hips + // InPlaceHipsBoneTrack = Hips + // we want to extract the Root value and set to the new root track so: + // Root = HipsBoneTrack - InPlaceHipsBoneTrack = (Root + Hips) - Hips = Root + for (int i = 0; i < NumOfKeys; ++i) + { + RootBoneTrack.PosKeys[i] = HipsTrackData.PosKeys[i] - InPlaceHipsTrackData.PosKeys[i]; + RootBoneTrack.RotKeys[i] = HipsTrackData.RotKeys[i] * InPlaceHipsTrackData.RotKeys[i].Inverse(); + RootBoneTrack.ScaleKeys[i] = FVector3f(1); + } + + IAnimationDataController& Controller = AnimSequence->GetController(); + constexpr bool bShouldTransact = false; + // NOTE: modifications MUST be done inside a "bracket", otherwise each modification will fire a re-build of the animation. + // After adding the "root" track, the re-build will fail since its track keys are missing. + // Worst: there's a bug in UE5.0 (https://github.com/EpicGames/UnrealEngine/blob/05ce24e3038cb1994a7c71d4d0058dbdb112f52b/Engine/Source/Runtime/Engine/Private/Animation/AnimSequenceHelpers.cpp#L593) + // where when no keys are present, element at index -1 is removed from an array, causing a random memory overriding. + Controller.OpenBracket(LOCTEXT("FMixamoAnimationRootMotionSolver_ExecuteExtraction_AnimEdit", "Animation editing"), bShouldTransact); + + // now we can replace the HipsBoneTrack with InPlaceHipsBoneTrack + Controller.SetBoneTrackKeys(FName("Hips"), InPlaceHipsTrackData.PosKeys, InPlaceHipsTrackData.RotKeys, InPlaceHipsTrackData.ScaleKeys, bShouldTransact); + + // add the new root track (now as the first item) + ensure(Controller.InsertBoneTrack(FName("root"), 0, bShouldTransact) == 0); + Controller.SetBoneTrackKeys(FName("root"), RootBoneTrack.PosKeys, RootBoneTrack.RotKeys, RootBoneTrack.ScaleKeys, bShouldTransact); + + // Apply all the changes at once. + Controller.CloseBracket(bShouldTransact); + + return true; +} + + + +float FMixamoAnimationRootMotionSolver::GetMaxBoneDisplacement(const UAnimSequence* AnimSequence, const FName& BoneName) +{ + UAnimDataModel* AnimDataModel = AnimSequence->GetDataModel(); + const FBoneAnimationTrack* HipsBoneTrack = AnimDataModel->FindBoneTrackByName(BoneName); + if (!HipsBoneTrack) + return 0; + + float MaxSize = 0; + + for (int i = 0; i < HipsBoneTrack->InternalTrackData.PosKeys.Num(); ++i) + { + float Size = HipsBoneTrack->InternalTrackData.PosKeys[i].Size(); + if (Size > MaxSize) + MaxSize = Size; + } + + return MaxSize; +} + + + +const UAnimSequence* FMixamoAnimationRootMotionSolver::EstimateInPlaceAnimation(const UAnimSequence* AnimationA, const UAnimSequence* AnimationB) +{ + FName RefBoneName(TEXT("Hips")); + + /* + Find the "in place" animation sequence. To do that we compare the two hips bone displacements. + The animation sequence with the lower value is the "in place" one. + @TODO: is this checks always reliable ? + */ + float dA = GetMaxBoneDisplacement(AnimationA, RefBoneName); + float dB = GetMaxBoneDisplacement(AnimationB, RefBoneName); + + const UAnimSequence* NormalAnimSequence = (dA < dB) ? AnimationB : AnimationA; + const UAnimSequence* InPlaceAnimSequence = (dA < dB) ? AnimationA : AnimationB; + + return InPlaceAnimSequence; +} diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.h new file mode 100644 index 00000000..87dd1dc1 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoAnimationRootMotionSolver.h @@ -0,0 +1,29 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Engine/EngineTypes.h" + + + +class USkeleton; +class UAnimSequence; + + + +class FMixamoAnimationRootMotionSolver +{ +public: + + void LaunchProcedureFlow(USkeleton* Skeleton); + + bool CanExecuteProcedure(const USkeleton* Skeleton) const; + +private: + + bool ExecuteExtraction(UAnimSequence* AnimSequence, const UAnimSequence* InPlaceAnimSequence); + + static float GetMaxBoneDisplacement(const UAnimSequence* AnimSequence, const FName& BoneName); + static const UAnimSequence* EstimateInPlaceAnimation(const UAnimSequence* AnimationA, const UAnimSequence* AnimationB); + +}; diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.cpp new file mode 100644 index 00000000..026dd40a --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.cpp @@ -0,0 +1,2247 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoSkeletonRetargeter.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "Animation/Skeleton.h" +#include "Engine/SkeletalMesh.h" + +#include "IKRigDefinition.h" +#include "RigEditor/IKRigController.h" +#include "RigEditor/IKRigDefinitionFactory.h" +#include "RetargetEditor/IKRetargeterController.h" +#include "RetargetEditor/IKRetargetFactory.h" +#include "Solvers/IKRig_PBIKSolver.h" + +#include "Rendering/SkeletalMeshModel.h" +#include "Rendering/SkeletalMeshLODModel.h" + +#include "PackageTools.h" + +#include "AssetRegistry/ARFilter.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "ScopedTransaction.h" +#include "Misc/ScopedSlowTask.h" + +#include "Retargeter/IKRetargeter.h" + +#include "SkeletonPoser.h" +#include "SkeletonMatcher.h" + +#include "Editor.h" +#include "SMixamoToolkitWidget.h" +#include "ComponentReregisterContext.h" +#include "Components/SkinnedMeshComponent.h" + + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + +// Define it to ignore the UE5 mannequin as a valid retarget source +//#define MAR_IGNORE_UE5_MANNEQUIN +#ifdef MAR_IGNORE_UE5_MANNEQUIN +# pragma message ("***WARNING*** Feature \"MetaHuman\" disabled.") +#endif + + +// Define it to disable the automatic addition of the Root Bone (needed to support UE4 Root Animations). +//#define MAR_ADDROOTBONE_DISABLE_ +#ifdef MAR_ADDROOTBONE_DISABLE_ +# pragma message ("***WARNING*** Feature \"AddRootBone\" disabled.") +#endif + +// Define it to disable the advance chains setup of the IK Retarger assets +//#define MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ +#ifdef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ +# pragma message ("***WARNING*** IK Retargeter is not using the advanced chains setup.") +#endif + +// Define it to disable the IK solvers setup of the IK Retarger assets +//#define MAR_IKRETARGETER_IKSOLVERS_DISABLE_ +#ifdef MAR_IKRETARGETER_IKSOLVERS_DISABLE_ +# pragma message ("***WARNING*** IK Retargeter is not using the IK solvers setup.") +#endif + +//#define MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_ +#ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_ +# pragma message ("***WARNING*** Preserving the Component Space bones of the upper arm bones is an experimental feature.") +#endif + + + +namespace +{ + + + +/** +Index of the last Mixamo bone, in kUE4MannequinToMixamo_BoneNamesMapping arrays, +used to determine if a skeleton is from Mixamo. + +Given the pair N-th, then index i = N * 2 + 1. +*/ +constexpr int IndexLastCheckedMixamoBone = 22 * 2 + 1; +/** +Index of the last UE Mannequin bone, in kUE4MannequinToMixamo_BoneNamesMapping arrays, +used to determine if a skeleton is the UE Mannequin. + +Given the pair N-th, then index i = N * 2 + 1. +*/ +constexpr int IndexLastCheckedUEMannequinBone = 22 * 2; + + + +/** +Mapping of "UE4 Mannequin" skeleton bones to the corresponding "Mixamo" skeleton bones names. + +NOTES: +- includes the added "root" bone (by default it's missing in Mixamo skeletons and it's added by the plugin). +- the first N pairs [ N = (IndexLastCheckedMixamoBone + 1) / 2 ] are used to + determine if a skeleton is from Mixamo. +*/ +static const char* const kUE4MannequinToMixamo_BoneNamesMapping[] = { + // UE Mannequin bone name MIXAMO bone name + "root", "root", + "pelvis", "Hips", + "spine_01", "Spine", + "spine_02", "Spine1", + "spine_03", "Spine2", + "neck_01", "Neck", + "head", "head", + "clavicle_l", "LeftShoulder", + "upperarm_l", "LeftArm", + "lowerarm_l", "LeftForeArm", + "hand_l", "LeftHand", + "clavicle_r", "RightShoulder", + "upperarm_r", "RightArm", + "lowerarm_r", "RightForeArm", + "hand_r", "RightHand", + "thigh_l", "LeftUpLeg", + "calf_l", "LeftLeg", + "foot_l", "LeftFoot", + "ball_l", "LeftToeBase", + "thigh_r", "RightUpLeg", + "calf_r", "RightLeg", + "foot_r", "RightFoot", + "ball_r", "RightToeBase", + // From here, ignored to determine if a skeleton is from Mixamo. + // From here, ignored to determine if a skeleton is from UE Mannequin. + "index_01_l", "LeftHandIndex1", + "index_02_l", "LeftHandIndex2", + "index_03_l", "LeftHandIndex3", + "middle_01_l", "LeftHandMiddle1", + "middle_02_l", "LeftHandMiddle2", + "middle_03_l", "LeftHandMiddle3", + "pinky_01_l", "LeftHandPinky1", + "pinky_02_l", "LeftHandPinky2", + "pinky_03_l", "LeftHandPinky3", + "ring_01_l", "LeftHandRing1", + "ring_02_l", "LeftHandRing2", + "ring_03_l", "LeftHandRing3", + "thumb_01_l", "LeftHandThumb1", + "thumb_02_l", "LeftHandThumb2", + "thumb_03_l", "LeftHandThumb3", + "index_01_r", "RightHandIndex1", + "index_02_r", "RightHandIndex2", + "index_03_r", "RightHandIndex3", + "middle_01_r", "RightHandMiddle1", + "middle_02_r", "RightHandMiddle2", + "middle_03_r", "RightHandMiddle3", + "pinky_01_r", "RightHandPinky1", + "pinky_02_r", "RightHandPinky2", + "pinky_03_r", "RightHandPinky3", + "ring_01_r", "RightHandRing1", + "ring_02_r", "RightHandRing2", + "ring_03_r", "RightHandRing3", + "thumb_01_r", "RightHandThumb1", + "thumb_02_r", "RightHandThumb2", + "thumb_03_r", "RightHandThumb3", + // Un-mapped bones (at the moment). Here for reference. + //"lowerarm_twist_01_l", nullptr, + //"upperarm_twist_01_l", nullptr, + //"lowerarm_twist_01_r", nullptr, + //"upperarm_twist_01_r", nullptr, + //"calf_twist_01_l", nullptr, + //"thigh_twist_01_l", nullptr, + //"calf_twist_01_r", nullptr, + //"thigh_twist_01_r", nullptr, + //"ik_foot_root", nullptr, + //"ik_foot_l", nullptr, + //"ik_foot_r", nullptr, + //"ik_hand_root", nullptr, + //"ik_hand_gun", nullptr, + //"ik_hand_l", nullptr, + //"ik_hand_r", nullptr, +}; + +constexpr int32 kUE4MannequinToMixamo_BoneNamesMapping_Num = sizeof(kUE4MannequinToMixamo_BoneNamesMapping) / sizeof(decltype(kUE4MannequinToMixamo_BoneNamesMapping[0])); +static_assert (kUE4MannequinToMixamo_BoneNamesMapping_Num % 2 == 0, "An event number of entries is expected"); + +static_assert (IndexLastCheckedMixamoBone % 2 == 1, "Mixamo indexes are odd numbers"); +static_assert (IndexLastCheckedMixamoBone >= 1, "First valid Mixamo index is 1"); +static_assert (IndexLastCheckedMixamoBone < kUE4MannequinToMixamo_BoneNamesMapping_Num, "Index out of bounds"); + +static_assert (IndexLastCheckedUEMannequinBone % 2 == 0, "UE Mannequin indexes are even numbers"); +static_assert (IndexLastCheckedUEMannequinBone >= 0, "First valid UE Mannequin index is 0"); +static_assert (IndexLastCheckedUEMannequinBone < kUE4MannequinToMixamo_BoneNamesMapping_Num, "Index out of bounds"); + + + +// UE5 mannequin bones in addition of the old mannequin. +// not all additional bones were included here (e.g fingers, etc) +static const char* const kUE5MannequinAdditionalBones[] = +{ + "spine_04", + "spine_05", + "neck_02", + "lowerarm_twist_02_l", + "lowerarm_twist_02_r", + "upperarm_twist_02_l", + "upperarm_twist_02_r", + "thigh_twist_02_l", + "thigh_twist_02_r", + "calf_twist_02_l", + "calf_twist_02_r" +}; + +/** +Mapping of "UE5 Mannequin" skeleton bones to the corresponding "Mixamo" skeleton bones names. + +NOTES: +- includes the added "root" bone (by default it's missing in Mixamo skeletons and it's added by the plugin). +*/ +static const char* const kUE5MannequinToMixamo_BoneNamesMapping[] = { + // UE Mannequin bone name MIXAMO bone name + "root", "root", + "pelvis", "Hips", + //"spine_01", nullptr, + "spine_02", "Spine", + "spine_03", "Spine1", + "spine_04", "Spine2", + //"spine_05", nullptr, + "neck_01", "Neck", + //"neck_02", nullptr, + "head", "head", + "clavicle_l", "LeftShoulder", + "upperarm_l", "LeftArm", + "lowerarm_l", "LeftForeArm", + "hand_l", "LeftHand", + "clavicle_r", "RightShoulder", + "upperarm_r", "RightArm", + "lowerarm_r", "RightForeArm", + "hand_r", "RightHand", + "thigh_l", "LeftUpLeg", + "calf_l", "LeftLeg", + "foot_l", "LeftFoot", + "ball_l", "LeftToeBase", + "thigh_r", "RightUpLeg", + "calf_r", "RightLeg", + "foot_r", "RightFoot", + "ball_r", "RightToeBase", + "index_01_l", "LeftHandIndex1", + "index_02_l", "LeftHandIndex2", + "index_03_l", "LeftHandIndex3", + "middle_01_l", "LeftHandMiddle1", + "middle_02_l", "LeftHandMiddle2", + "middle_03_l", "LeftHandMiddle3", + "pinky_01_l", "LeftHandPinky1", + "pinky_02_l", "LeftHandPinky2", + "pinky_03_l", "LeftHandPinky3", + "ring_01_l", "LeftHandRing1", + "ring_02_l", "LeftHandRing2", + "ring_03_l", "LeftHandRing3", + "thumb_01_l", "LeftHandThumb1", + "thumb_02_l", "LeftHandThumb2", + "thumb_03_l", "LeftHandThumb3", + "index_01_r", "RightHandIndex1", + "index_02_r", "RightHandIndex2", + "index_03_r", "RightHandIndex3", + "middle_01_r", "RightHandMiddle1", + "middle_02_r", "RightHandMiddle2", + "middle_03_r", "RightHandMiddle3", + "pinky_01_r", "RightHandPinky1", + "pinky_02_r", "RightHandPinky2", + "pinky_03_r", "RightHandPinky3", + "ring_01_r", "RightHandRing1", + "ring_02_r", "RightHandRing2", + "ring_03_r", "RightHandRing3", + "thumb_01_r", "RightHandThumb1", + "thumb_02_r", "RightHandThumb2", + "thumb_03_r", "RightHandThumb3", + // Un-mapped bones (at the moment). Here for reference. + //"lowerarm_twist_01_l", nullptr, + //"upperarm_twist_01_l", nullptr, + //"lowerarm_twist_01_r", nullptr, + //"upperarm_twist_01_r", nullptr, + //"calf_twist_01_l", nullptr, + //"thigh_twist_01_l", nullptr, + //"calf_twist_01_r", nullptr, + //"thigh_twist_01_r", nullptr, + //"ik_foot_root", nullptr, + //"ik_foot_l", nullptr, + //"ik_foot_r", nullptr, + //"ik_hand_root", nullptr, + //"ik_hand_gun", nullptr, + //"ik_hand_l", nullptr, + //"ik_hand_r", nullptr, +}; + +constexpr int32 kUE5MannequinToMixamo_BoneNamesMapping_Num = sizeof(kUE5MannequinToMixamo_BoneNamesMapping) / sizeof(decltype(kUE5MannequinToMixamo_BoneNamesMapping[0])); +static_assert (kUE5MannequinToMixamo_BoneNamesMapping_Num % 2 == 0, "An event number of entries is expected"); + + + +/** +Names of bones in the Mixamo skeleton that must preserve their Component Space transform (relative to the parent) +when re-posed to match the UE Mannequin skeleton base pose. +*/ +static const TArray Mixamo_PreserveComponentSpacePose_BoneNames = { + "Head", + "LeftToeBase", + "RightToeBase" + +#ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_ + ,"RightShoulder" + ,"RightArm" + ,"LeftShoulder" + ,"LeftArm" +#endif +}; + +/** +Names of bones in the UE4/UE5/MetaHuman Mannequin skeleton that must preserve their Component Space transform (relative to the parent) +when re-posed to match the Mixamo skeleton base pose. +*/ +static const TArray UEMannequin_PreserveComponentSpacePose_BoneNames = { + "head", + "neck_02", // This is used only by UE5/MetaHuman skeletons. UE4 doens't have it so it should be simply ignored. + "ball_r", + "ball_l" + +#ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_ + ,"clavicle_r" + ,"upperarm_r" + ,"clavicle_l" + ,"upperarm_l" +#endif +}; + + + +/** +Parent-child pair bone's names in the Mixamo skeleton where the child bone must be forcefully oriented +when re-posed to match the UE Mannequin skeleton base pose regardless of the children number of the parent bone. +*/ +static const TArray> Mixamo_ParentChildBoneNamesToBypassOneChildConstraint = { + {"LeftUpLeg", "LeftLeg"}, + {"LeftLeg", "LeftFoot"}, + {"LeftFoot", "LeftToeBase"}, + {"LeftToeBase", "LeftToe_End"}, + {"RightUpLeg", "RightLeg"}, + {"RightLeg", "RightFoot"}, + {"RightFoot", "RightToeBase"}, + {"RightToeBase", "RightToe_End"}, + {"Hips", "Spine"}, // Heuristic to try to align better the part. + {"Spine", "Spine1"}, + {"Spine1", "Spine2"}, + {"Spine2", "Neck"}, // Heuristic to try to align better the part. + {"Neck", "Head"}, + {"Head", "HeadTop_End"}, + {"LeftShoulder", "LeftArm"}, + {"LeftArm", "LeftForeArm"}, + {"LeftForeArm", "LeftHand"}, + {"LeftHand", "LeftHandMiddle1"}, // Heuristic to try to align better the part. + {"LeftHandIndex1", "LeftHandIndex2"}, + {"LeftHandIndex2", "LeftHandIndex3"}, + {"LeftHandIndex3", "LeftHandIndex4"}, + {"LeftHandMiddle1", "LeftHandMiddle2"}, + {"LeftHandMiddle2", "LeftHandMiddle3"}, + {"LeftHandMiddle3", "LeftHandMiddle4"}, + {"LeftHandPinky1", "LeftHandPinky2"}, + {"LeftHandPinky2", "LeftHandPinky3"}, + {"LeftHandPinky3", "LeftHandPinky4"}, + {"LeftHandRing1", "LeftHandRing2"}, + {"LeftHandRing2", "LeftHandRing3"}, + {"LeftHandRing3", "LeftHandRing4"}, + {"LeftHandThumb1", "LeftHandThumb2"}, + {"LeftHandThumb2", "LeftHandThumb3"}, + {"LeftHandThumb3", "LeftHandThumb4"}, + {"RightShoulder", "RightArm"}, + {"RightArm", "RightForeArm"}, + {"RightForeArm", "RightHand"}, + {"RightHand", "RightHandMiddle1"}, // Heuristic to try to align better the part. + {"RightHandIndex1", "RightHandIndex2"}, + {"RightHandIndex2", "RightHandIndex3"}, + {"RightHandIndex3", "RightHandIndex4"}, + {"RightHandMiddle1", "RightHandMiddle2"}, + {"RightHandMiddle2", "RightHandMiddle3"}, + {"RightHandMiddle3", "RightHandMiddle4"}, + {"RightHandPinky1", "RightHandPinky2"}, + {"RightHandPinky2", "RightHandPinky3"}, + {"RightHandPinky3", "RightHandPinky4"}, + {"RightHandRing1", "RightHandRing2"}, + {"RightHandRing2", "RightHandRing3"}, + {"RightHandRing3", "RightHandRing4"}, + {"RightHandThumb1", "RightHandThumb2"}, + {"RightHandThumb2", "RightHandThumb3"}, + {"RightHandThumb3", "RightHandThumb4"} +}; + +/** +Parent-child pair bone's names in the UE4 Mannequin skeleton where the child bone must be forcefully oriented +when re-posed to match the Mixamo skeleton base pose regardless of the children number of the parent bone. +*/ +static const TArray> UE4Mannequin_ParentChildBoneNamesToBypassOneChildConstraint = { + {"pelvis", "spine_01"}, // Heuristic to try to align better the part. + {"spine_01", "spine_02"}, + {"spine_02", "spine_03"}, + {"spine_03", "neck_01"}, + {"neck_01", "head"}, + {"thigh_l", "calf_l"}, // to ignore "thigh_twist_01_l" + {"calf_l", "foot_l"}, // to ignore "calf_twist_01_l" + {"foot_l", "ball_l"}, + {"thigh_r", "calf_r"}, // to ignore "thigh_twist_01_r" + {"calf_r", "foot_r"}, // to ignore "calf_twist_01_r" + {"foot_r", "ball_r"}, + {"clavicle_l", "upperarm_l"}, + {"upperarm_l", "lowerarm_l"}, // to ignore "upperarm_twist_01_l" + {"lowerarm_l", "hand_l"}, // to ignore "lowerarm_twist_01_l" + {"hand_l", "middle_01_l"}, // Heuristic to try to align better the part. + {"index_01_l", "index_02_l"}, + {"index_02_l", "index_03_l"}, + {"middle_01_l", "middle_02_l"}, + {"middle_02_l", "middle_03_l"}, + {"pinky_01_l", "pinky_02_l"}, + {"pinky_02_l", "pinky_03_l"}, + {"ring_01_l", "ring_02_l"}, + {"ring_02_l", "ring_03_l"}, + {"thumb_01_l", "thumb_02_l"}, + {"thumb_02_l", "thumb_03_l"}, + {"clavicle_r", "upperarm_r"}, + {"upperarm_r", "lowerarm_r"}, // to ignore "upperarm_twist_01_r" + {"lowerarm_r", "hand_r"}, // to ignore "lowerarm_twist_01_r" + {"hand_r", "middle_01_r"}, // Heuristic to try to align better the part. + {"index_01_r", "index_02_r"}, + {"index_02_r", "index_03_r"}, + {"middle_01_r", "middle_02_r"}, + {"middle_02_r", "middle_03_r"}, + {"pinky_01_r", "pinky_02_r"}, + {"pinky_02_r", "pinky_03_r"}, + {"ring_01_r", "ring_02_r"}, + {"ring_02_r", "ring_03_r"}, + {"thumb_01_r", "thumb_02_r"}, + {"thumb_02_r", "thumb_03_r"} +}; + +/** +Parent-child pair bone's names in the UE5/MetaHuman Mannequin skeleton where the child bone must be forcefully oriented +when re-posed to match the Mixamo skeleton base pose regardless of the children number of the parent bone. +*/ +static const TArray> UE5Mannequin_ParentChildBoneNamesToBypassOneChildConstraint = { + {"pelvis", "spine_01"}, // Heuristic to try to align better the part. + {"spine_01", "spine_02"}, + {"spine_02", "spine_03"}, + {"spine_03", "spine_04"}, + {"spine_04", "spine_05"}, + {"spine_05", "neck_01"}, // Heuristic to try to align better the part. + {"neck_01", "neck_02"}, + {"neck_02", "head"}, + {"thigh_l", "calf_l"}, + {"calf_l", "foot_l"}, + {"foot_l", "ball_l"}, + {"thigh_r", "calf_r"}, + {"calf_r", "foot_r"}, + {"foot_r", "ball_r"}, + {"clavicle_l", "upperarm_l"}, + {"upperarm_l", "lowerarm_l"}, + {"lowerarm_l", "hand_l"}, + {"hand_l", "middle_metacarpal_l"}, // Heuristic to try to align better the part. + {"index_metacarpal_l", "index_01_l"}, + {"index_01_l", "index_02_l"}, + {"index_02_l", "index_03_l"}, + {"middle_metacarpal_l", "middle_01_l"}, + {"middle_01_l", "middle_02_l"}, + {"middle_02_l", "middle_03_l"}, + {"pinky_metacarpal_l", "pinky_01_l"}, + {"pinky_01_l", "pinky_02_l"}, + {"pinky_02_l", "pinky_03_l"}, + {"ring_metacarpal_l", "ring_01_l"}, + {"ring_01_l", "ring_02_l"}, + {"ring_02_l", "ring_03_l"}, + {"thumb_01_l", "thumb_02_l"}, + {"thumb_02_l", "thumb_03_l"}, + {"clavicle_r", "upperarm_r"}, + {"upperarm_r", "lowerarm_r"}, + {"lowerarm_r", "hand_r"}, + {"hand_r", "middle_metacarpal_r"}, // Heuristic to try to align better the part. + {"index_metacarpal_r", "index_01_r"}, + {"index_01_r", "index_02_r"}, + {"index_02_r", "index_03_r"}, + {"middle_metacarpal_r", "middle_01_r"}, + {"middle_01_r", "middle_02_r"}, + {"middle_02_r", "middle_03_r"}, + {"pinky_metacarpal_r", "pinky_01_r"}, + {"pinky_01_r", "pinky_02_r"}, + {"pinky_02_r", "pinky_03_r"}, + {"ring_metacarpal_r", "ring_01_r"}, + {"ring_01_r", "ring_02_r"}, + {"ring_02_r", "ring_03_r"}, + {"thumb_01_r", "thumb_02_r"}, + {"thumb_02_r", "thumb_03_r"} +}; + +static const FName RootBoneName("root"); + + + +#ifndef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ +/** +Mapping of "UE4/UE5/MetaHuman Mannequin" chain names to the corresponding "Mixamo" chain names. +*/ +static const char* const kUEMannequinToMixamo_ChainNamesMapping[] = { + "Root", "Root", + "Spine", "Spine", + "Head", "Head", + "LeftClavicle", "LeftClavicle", + "RightClavicle", "RightClavicle", + "LeftArm", "LeftArm", + "RightArm", "RightArm", + "LeftLeg", "LeftLeg", + "RightLeg", "RightLeg", + "LeftIndex", "LeftIndex", + "RightIndex", "RightIndex", + "LeftMiddle", "LeftMiddle", + "RightMiddle", "RightMiddle", + "LeftPinky", "LeftPinky", + "RightPinky", "RightPinky", + "LeftRing", "LeftRing", + "RightRing", "RightRing", + "LeftThumb", "LeftThumb", + "RightThumb", "RightThumb" +}; + +constexpr int32 kUEMannequinToMixamo_ChainNamesMapping_Num = sizeof(kUEMannequinToMixamo_ChainNamesMapping) / sizeof(decltype(kUEMannequinToMixamo_ChainNamesMapping[0])); +static_assert (kUEMannequinToMixamo_ChainNamesMapping_Num % 2 == 0, "An event number of entries is expected"); +#endif + + +/** +List of "chain names" (relative to the UE4/UE5/MetaHuman Mannequin names) that must not be configured in the IKRetarget asset. +*/ +static const TArray UEMannequin_SkipChains_ChainNames = { +#ifdef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + TEXT("root"), + TEXT("pelvis") +#endif +}; + + +/** +List of "chain names" (relative to the UE4/UE5/MetaHuman Mannequin names) for which the "Drive IK Goal" must be configured. +*/ +static const TArray UEMannequin_DriveIKGoal_ChainNames = { + "LeftArm", + "RightArm", + "LeftLeg", + "RightLeg" +}; + + +/** +List of "chain names" (relative to the UE4/UE5/MetaHuman Mannequin names) for which the "one to one" must be set as FK Rotation mode. +*/ +static const TArray UEMannequin_OneToOneFKRotationMode_ChainNames = { + "LeftIndex", + "RightIndex", + "LeftMiddle", + "RightMiddle", + "LeftPinky", + "RightPinky", + "LeftRing", + "RightRing", + "LeftThumb", + "RightThumb", +}; + + + +#ifndef MAR_IGNORE_UE5_MANNEQUIN + +static const FSoftObjectPath kMetaHumanBaseSkeleton_ObjectPath(TEXT("/Game/MetaHumans/Common/Female/Medium/NormalWeight/Body/metahuman_base_skel.metahuman_base_skel")); +static const FSoftObjectPath kMetaHumanDefaultSkeletalMesh_ObjectPath(TEXT("/Game/MetaHumans/Common/Female/Medium/NormalWeight/Body/f_med_nrw_body.f_med_nrw_body")); + +#endif // MAR_IGNORE_UE5_MANNEQUIN + + + +/// Returns the "cleaned" name of a skeleton asset. +FString GetBaseSkeletonName(const USkeleton* Skeleton) +{ + check(Skeleton != nullptr); + FString Name = Skeleton->GetName(); + Name.RemoveFromStart(TEXT("SK_")); + Name.RemoveFromEnd(TEXT("Skeleton")); + Name.RemoveFromEnd(TEXT("_")); + return Name; +} + +/// Returns a nicer name for the IKRig asset associated to Skeleton. +FString GetRigName(const USkeleton* Skeleton) +{ + return FString::Printf(TEXT("IK_%s"), *GetBaseSkeletonName(Skeleton)); +} + + +/// Returns a nicer name for the IKRetargeter asset used to retarget from ReferenceSkeleton to Skeleton. +FString GetRetargeterName(const USkeleton* ReferenceSkeleton, const USkeleton* Skeleton) +{ + return FString::Printf(TEXT("RTG_%s_%s"), *GetBaseSkeletonName(ReferenceSkeleton), *GetBaseSkeletonName(Skeleton)); +} + + + +// See FSkeletonPoser::ComputeComponentSpaceTransform(). +// TODO: this is used by ConfigureBoneLimitsLocalToBS(); if we'll keep this function, it could be helpful to make the above one public and use it. +FTransform ComputeComponentSpaceTransform(const FReferenceSkeleton & RefSkeleton, int32 BoneIndex) +{ + if (BoneIndex == INDEX_NONE) + { + return FTransform::Identity; + } + + const TArray& RelTransforms = RefSkeleton.GetRefBonePose(); + FTransform T = RelTransforms[BoneIndex]; + int32 i = RefSkeleton.GetParentIndex(BoneIndex); + while (i != INDEX_NONE) + { + T *= RelTransforms[i]; + i = RefSkeleton.GetParentIndex(i); + } + + return T; +} + + + +/** +Configure the bone preferred angle converting from the input "Local Space" to the Skeleton Bone Space. + +Local space is constructed with the forward vector pointing to the bone direction (the direction pointing the child bone), +the input right vector (BoneLimitRightCS) and the Up vector as the cross product of the two. + +Preferred angles are then remapped from these axis to the **matching** skeleton bone space axis. + +Bone name is in Settings. +*/ +void ConfigureBonePreferredAnglesLocalToBS( + const USkeleton* Skeleton, + UIKRig_PBIKBoneSettings* Settings, + FName ChildBoneName, + FVector PreferredAnglesLS, + FVector BoneLimitRightCS +) +{ + check(Skeleton != nullptr); + if (Settings == nullptr) + { + return; + } + const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton(); + const int32 BoneIndex = RefSkeleton.FindBoneIndex(Settings->Bone); + // Skip if required data are missing. + if (BoneIndex == INDEX_NONE) + { + return; + } + + const int32 ChildBoneIndex = RefSkeleton.FindBoneIndex(ChildBoneName); + if (ChildBoneIndex == INDEX_NONE) + { + return; + } + + check(RefSkeleton.GetParentIndex(ChildBoneIndex) == BoneIndex); + + auto GetMatchingAxis = [](const TArray& axis, const FVector& refAxis, bool& bInverted) + { + float ProjOnDir = FVector::DotProduct(axis[0], refAxis); + int bestIdx = 0; + for(int i = 1; i < 3; ++i) + { + float dot = FVector::DotProduct(axis[i], refAxis); + if (FMath::Abs(dot) > FMath::Abs(ProjOnDir)) + { + ProjOnDir = dot; + bestIdx = i; + } + } + + bInverted = ProjOnDir < 0; + return bestIdx; + }; + + const FTransform BoneCS = ComputeComponentSpaceTransform(RefSkeleton, BoneIndex); + const FTransform ChildBoneCS = ComputeComponentSpaceTransform(RefSkeleton, ChildBoneIndex); + const FQuat& BoneR = BoneCS.GetRotation(); + + const FVector XAxisCS = BoneCS.GetUnitAxis(EAxis::X); + const FVector YAxisCS = BoneCS.GetUnitAxis(EAxis::Y); + const FVector ZAxisCS = BoneCS.GetUnitAxis(EAxis::Z); + + const FVector ParentToChildDirCS = (ChildBoneCS.GetTranslation() - BoneCS.GetTranslation()).GetSafeNormal(); + + // NOTE: [XBoneLimitCS, YBoneLimitCS, ZBoneLimitCS] could be NOT an orthonormal basis! + const FVector XBoneLimitCS = ParentToChildDirCS; + const FVector YBoneLimitCS = BoneLimitRightCS; + const FVector ZBoneLimitCS = FVector::CrossProduct(XBoneLimitCS, YBoneLimitCS); + check(!ZBoneLimitCS.IsNearlyZero()); + + bool bInverted[3]; + int remappedAxis[3]; + + remappedAxis[0] = GetMatchingAxis({ XAxisCS , YAxisCS , ZAxisCS }, XBoneLimitCS, bInverted[0]); + remappedAxis[1] = GetMatchingAxis({ XAxisCS , YAxisCS , ZAxisCS }, YBoneLimitCS, bInverted[1]); + remappedAxis[2] = GetMatchingAxis({ XAxisCS , YAxisCS , ZAxisCS }, ZBoneLimitCS, bInverted[2]); + + for (int i = 0; i < 3; ++i) + { + int axisIdx = remappedAxis[i]; + float angle = PreferredAnglesLS[i]; + float sign = 1.0f; + + if (bInverted[i]) + sign = -1.0f; + if(((i == 2) ^ (axisIdx == 2)) == true) + sign *= -1.0f; + + Settings->PreferredAngles[axisIdx] = angle * sign; + } + + Settings->bUsePreferredAngles = true; +} + + + +// A #define since I don't want to copy buffers/objects, and doing it with C++ template specialization is a mess... +#define SelectBySkeletonType(SkeletonType,UE4Value,UE5Value) ((SkeletonType) == ETargetSkeletonType::ST_UE5_MANNEQUIN ? UE5Value : UE4Value) + +} // namespace *unnamed* + + + +FMixamoSkeletonRetargeter::FMixamoSkeletonRetargeter() + : UE4MannequinToMixamo_BoneNamesMapping(kUE4MannequinToMixamo_BoneNamesMapping, kUE4MannequinToMixamo_BoneNamesMapping_Num), +#ifdef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + UE4MannequinToMixamo_ChainNamesMapping(kUE4MannequinToMixamo_BoneNamesMapping, kUE4MannequinToMixamo_BoneNamesMapping_Num), +#else + UE4MannequinToMixamo_ChainNamesMapping(kUEMannequinToMixamo_ChainNamesMapping, kUEMannequinToMixamo_ChainNamesMapping_Num), +#endif + UE5MannequinToMixamo_BoneNamesMapping(kUE5MannequinToMixamo_BoneNamesMapping, kUE5MannequinToMixamo_BoneNamesMapping_Num), +#ifdef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + UE5MannequinToMixamo_ChainNamesMapping(kUE5MannequinToMixamo_BoneNamesMapping, kUE5MannequinToMixamo_BoneNamesMapping_Num) +#else + UE5MannequinToMixamo_ChainNamesMapping(kUEMannequinToMixamo_ChainNamesMapping, kUEMannequinToMixamo_ChainNamesMapping_Num) +#endif +{ +} + + + +/** +Retarget all the Skeletons (Mixamo skeletons) to a UE Mannequin skeleton that the user will interactively select. +*/ +void FMixamoSkeletonRetargeter::RetargetToUE4Mannequin(TArray Skeletons) const +{ + if (Skeletons.Num() <= 0) + { + return; + } + + // Get the UE4 "Mannequin" skeleton. + USkeleton * ReferenceSkeleton = AskUserForTargetSkeleton(); + if (ReferenceSkeleton == nullptr) + { + // We hadn't found a suitable skeleton. + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("No suitable Skeleton selected. Retargeting aborted."))); + return; + } + + TArray AssetsToOverwrite; + for (USkeleton* Skeleton : Skeletons) + { + EnumerateAssetsToOverwrite(Skeleton, ReferenceSkeleton, AssetsToOverwrite); + } + if (AssetsToOverwrite.Num() && !AskUserOverridingAssetsConfirmation(AssetsToOverwrite)) + { + FMessageLog("LogMixamoToolkit").Error(FText::FromString(TEXT("Files overwritten denied. Retargeting aborted by the user."))); + return; + } + + // Ensure that the ReferenceSkeleton has a preview mesh! + // Without it, retargeting an animation will fail (CreateUEMannequinIKRig -> CreateIKRig -> will be unable to get a required skeletal mesh) + const ETargetSkeletonType ReferenceSkeletonType = GetTargetSkeletonType(ReferenceSkeleton); + bool bSetPreviewMesh = ( ReferenceSkeleton->GetPreviewMesh() == nullptr ); + +#ifndef MAR_IGNORE_UE5_MANNEQUIN + const bool bIsMetaHumanSkeleton = ( + ReferenceSkeletonType == ETargetSkeletonType::ST_UE5_MANNEQUIN + && FSoftObjectPath(ReferenceSkeleton->GetPathName()) == kMetaHumanBaseSkeleton_ObjectPath + ); + + // If targetting the MetaHuman skeleton ("metahuman_base_skel"), ensure it's + // using the default skeletal mesh "f_med_nrw_body" as preview. + // + // Code in Retarget() will process only "f_med_nrw_body" and will filter out all + // the other skeletal meshes; but later code relies on the Preview Mesh for some + // computations resulting in an error if the returned skeletal mesh has not been + // processed. + // While it would be better to forcefully use "f_med_nrw_body" without changing + // any Preview Mesh set by the user, for now this is the simplest solution to not + // revolutionize existing code. + if (bIsMetaHumanSkeleton) + { + bSetPreviewMesh = ( + ReferenceSkeleton->GetPreviewMesh() == nullptr + || !(FSoftObjectPath(ReferenceSkeleton->GetPreviewMesh()->GetPathName()) == kMetaHumanDefaultSkeletalMesh_ObjectPath) + ); + } +#endif + + if (bSetPreviewMesh) + { + TArray ReferenceSkeletalMeshes; + GetAllSkeletalMeshesUsingSkeleton(ReferenceSkeleton, ReferenceSkeletalMeshes); + +#ifndef MAR_IGNORE_UE5_MANNEQUIN + // In case of a MetaHuman skeleton, try to pick a good preview mesh (move it at index 0). + if (bIsMetaHumanSkeleton) + { + auto RepositionSkeletalMesheByObjectPath = [&](const FSoftObjectPath& Query) { + int32 i = ReferenceSkeletalMeshes.IndexOfByPredicate( + [&](const FAssetData& A) { + return A.GetSoftObjectPath() == Query; + } + ); + if (i == INDEX_NONE) + { + return false; + } + ReferenceSkeletalMeshes.Swap(i, 0); + return true; + }; + + if (!RepositionSkeletalMesheByObjectPath(kMetaHumanDefaultSkeletalMesh_ObjectPath)) + { + const FString ErrorMessage = FString::Format(TEXT("Default MetaHuman skeletal mesh '{0}' not found. Retargeting aborted."), + { kMetaHumanDefaultSkeletalMesh_ObjectPath.ToString() }); + + //RepositionSkeletalMesheByObjectPath(TEXT("/Game/MetaHumans/Common/Male/Medium/NormalWeight/Body/m_med_nrw_body.m_med_nrw_body")); + FMessageLog("LogMixamoToolkit").Error(FText::FromString(ErrorMessage)); + return; + } + } +#endif // MAR_IGNORE_UE5_MANNEQUIN + + // This will load the Skeletal Mesh. + ReferenceSkeleton->SetPreviewMesh(CastChecked(ReferenceSkeletalMeshes[0].GetAsset())); + } + + // Process all input skeletons. + FScopedSlowTask Progress(Skeletons.Num(), LOCTEXT("FMixamoSkeletonRetargeter_ProgressTitle", "Retargeting of Mixamo assets")); + Progress.MakeDialog(); + const FScopedTransaction Transaction(LOCTEXT("FMixamoSkeletonRetargeter_RetargetSkeletons", "Retargeting of Mixamo assets")); + for (USkeleton * Skeleton : Skeletons) + { + Progress.EnterProgressFrame(1, FText::FromName(Skeleton->GetFName())); + Retarget(Skeleton, ReferenceSkeleton, ReferenceSkeletonType); + } +} + + + +/// Return true if Skeleton is a Mixamo skeleton. +bool FMixamoSkeletonRetargeter::IsMixamoSkeleton(const USkeleton * Skeleton) const +{ + // We consider a Skeleton "coming from Mixamo" if it has at least X% of the expected bones. + const float MINIMUM_MATCHING_PERCENTAGE = .75f; + + // Convert the array of expected bone names (TODO: cache it...). + TArray BoneNames; + UE4MannequinToMixamo_BoneNamesMapping.GetDestination(BoneNames); + // Look for and count the known Mixamo bones (see comments on IndexLastCheckedMixamoBone and UEMannequinToMixamo_BonesMapping). + constexpr int32 NumBones = (IndexLastCheckedMixamoBone + 1) / 2; + BoneNames.SetNum(NumBones); + + FSkeletonMatcher SkeletonMatcher(BoneNames, MINIMUM_MATCHING_PERCENTAGE); + return SkeletonMatcher.IsMatching(Skeleton); +} + + + +/// Return true if AssetData is NOT a UE Mannequin skeleton asset. +bool FMixamoSkeletonRetargeter::OnShouldFilterNonUEMannequinSkeletonAsset(const FAssetData& AssetData) const +{ + // Skip non skeleton assets. + if (!AssetData.IsInstanceOf(USkeleton::StaticClass())) + { + return false; + } + +#ifndef MAR_IGNORE_UE5_MANNEQUIN + // Special filtering for skeletons in '/Game/MetaHumans/' path: inside this path, + // we want to show only the "metahuman_base_skel" (corresponding to the Female-Medium-NormalWeight body). + // + // This because all the MetaHuman skeletal meshes are based on it (see ), + // so any other skeleton here can/must be ignored. + // + // In particular, downloaded accessories (tested with some characters manually downloaded from the Quixel Website) + // can have skeletons compatible with "metahuman_base_skel" (in particular for clothes like "tops", e.g. + // "/Game/MetaHumans/Common/Male/Tall/OverWeight/Tops/Hoodie/Meshes/m_tal_ovw_top_hoodie_Skeleton"). + // At run-time, their animation are instead driven by "metahuman_base_skel": in the MetaHuman actor blueprint + // (e.g. "/Game/MetaHumans/Hudson/BP_Hudson"), in the Construction Script, the function "EnableMasterPose" + // is called for all the skeletal mesh components forcing them to use the "Body" as Master Pose Component + // (https://docs.unrealengine.com/5.0/en-US/modular-characters-in-unreal-engine/); this causes that all the animations + // will be driven by the "Body" skeletal mesh that is configured to use "metahuman_base_skel". + // Since they're not used at run-time for animations purposes, it's pointless to select them for retargeting. + if (AssetData.PackagePath.ToString().StartsWith(TEXT("/Game/MetaHumans/"), ESearchCase::IgnoreCase)) + { + return !(AssetData.GetSoftObjectPath() == kMetaHumanBaseSkeleton_ObjectPath); + } +#endif + + // To check the skeleton bones, unfortunately we've to load the asset. + USkeleton* Skeleton = Cast(AssetData.GetAsset()); + return Skeleton != nullptr ? !IsUEMannequinSkeleton(Skeleton) : true; +} + + + +ETargetSkeletonType FMixamoSkeletonRetargeter::GetTargetSkeletonType(const USkeleton* Skeleton) const +{ + // We consider a Skeleton "being the UE Mannequin" if it has at least X% of the expected bones. + const float MINIMUM_MATCHING_PERCENTAGE = .75f; + + // Convert the array of expected bone names + /// @TODO: cache it. + TArray BoneNames; + UE4MannequinToMixamo_BoneNamesMapping.GetSource(BoneNames); + // Look for and count the known UE Mannequin bones (see comments on IndexLastCheckedUEMannequinBone and UEMannequinToMixamo_BonesMapping). + constexpr int32 NumBones = (IndexLastCheckedUEMannequinBone + 2) / 2; + BoneNames.SetNum(NumBones); + + FSkeletonMatcher SkeletonMatcher(BoneNames, MINIMUM_MATCHING_PERCENTAGE); + if (SkeletonMatcher.IsMatching(Skeleton)) + { + // It can be an UE4 or an UE5/MetaHuman skeleton, disambiguate it. + TArray UE5BoneNames; + for (int i = 0; i < sizeof(kUE5MannequinAdditionalBones) / sizeof(decltype(kUE5MannequinAdditionalBones[0])); ++i) + { + UE5BoneNames.Add(kUE5MannequinAdditionalBones[i]); + } + + const float MINIMUM_MATCHING_PERCENTAGE_UE5 = .25f; + FSkeletonMatcher SkeletonMatcherUE5(UE5BoneNames, MINIMUM_MATCHING_PERCENTAGE_UE5); + + return SkeletonMatcherUE5.IsMatching(Skeleton) ? ETargetSkeletonType::ST_UE5_MANNEQUIN : ETargetSkeletonType::ST_UE4_MANNEQUIN; + } + + return ETargetSkeletonType::ST_UNKNOWN; +} + + + +/// Return true if Skeleton is a UE Mannequin skeleton. +bool FMixamoSkeletonRetargeter::IsUEMannequinSkeleton(const USkeleton* Skeleton) const +{ + const ETargetSkeletonType Type = GetTargetSkeletonType(Skeleton); + return + Type == ETargetSkeletonType::ST_UE4_MANNEQUIN +#ifndef MAR_IGNORE_UE5_MANNEQUIN + || Type == ETargetSkeletonType::ST_UE5_MANNEQUIN +#endif + ; +} + + + +/** +Process Skeleton to support retargeting to ReferenceSkeleton. + +Usually this requires to process all the Skeletal Meshes based on Skeleton. +*/ +void FMixamoSkeletonRetargeter::Retarget( + USkeleton* Skeleton, + const USkeleton * ReferenceSkeleton, + ETargetSkeletonType ReferenceSkeletonType +) const +{ + check(Skeleton != nullptr); + check(ReferenceSkeleton != nullptr); + + FMessageLog("LogMixamoToolkit").Info(FText::FromString(FString::Format(TEXT("Retargeting Mixamo skeleton '{0}'"), + { *Skeleton->GetName() }))); + + // Check for a skeleton retargeting on itself. + if (Skeleton == ReferenceSkeleton) + { + FMessageLog("LogMixamoToolkit").Warning(FText::FromString(FString::Format(TEXT("Skipping retargeting of Mixamo skeleton '{0}' on itself"), + { *Skeleton->GetName() }))); + return; + } + + // Check for invalid root bone (root bone not at position 0) + if (HasFakeRootBone(Skeleton)) + { + FMessageLog("LogMixamoToolkit").Warning(FText::FromString(FString::Format(TEXT("Skipping retargeting of Mixamo skeleton '{0}'; invalid 'root' bone at index != 0"), + { *Skeleton->GetName() }))); + return; + } + + // Get all USkeletalMesh assets using Skeleton (i.e. the Mixamo skeleton). + TArray SkeletalMeshes; + GetAllSkeletalMeshesUsingSkeleton(Skeleton, SkeletalMeshes); + + /* + Retargeting uses the SkeletalMesh's reference skeleton, as it counts for mesh proportions. + If you need to use the original Skeleton, you have to ensure Skeleton pose has the same proportions + of the skeletal mesh we are retargeting calling: + Skeleton->GetPreviewMesh(true)->UpdateReferencePoseFromMesh(SkeletonMesh); + */ + // Add the root bone if needed. This: fixes a offset glitch in the animations, is generally useful. +#ifndef MAR_ADDROOTBONE_DISABLE_ + AddRootBone(Skeleton, SkeletalMeshes); +#endif + + // Be sure that the Skeleton has a preview mesh! + // without it, retargeting an animation will fail + if (SkeletalMeshes.Num() > 0) + SetPreviewMesh(Skeleton, SkeletalMeshes[0]); + + // Create the IKRig assets, one for each input skeleton. + UIKRigDefinition* MixamoRig = CreateMixamoIKRig(Skeleton); + UIKRigDefinition* UEMannequinRig = CreateUEMannequinIKRig(ReferenceSkeleton, ReferenceSkeletonType); + + // Create the IKRetarget asset to retarget from the UE Mannequin to Mixamo. + const FString SkeletonBasePackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + const FStaticNamesMapper & UEMannequinToMixamo_ChainNamesMapping = SelectBySkeletonType(ReferenceSkeletonType, UE4MannequinToMixamo_ChainNamesMapping, UE5MannequinToMixamo_ChainNamesMapping); + const FStaticNamesMapper MixamoToUEMannequin_ChainNamesMapping = UEMannequinToMixamo_ChainNamesMapping.GetInverseMapper(); + UIKRetargeter* IKRetargeter_UEMannequinToMixamo = CreateIKRetargeter( + SkeletonBasePackagePath, + GetRetargeterName(ReferenceSkeleton, Skeleton), + UEMannequinRig, + MixamoRig, + MixamoToUEMannequin_ChainNamesMapping, + UEMannequinToMixamo_ChainNamesMapping.MapNames(UEMannequin_SkipChains_ChainNames), + UEMannequinToMixamo_ChainNamesMapping.MapNames(UEMannequin_DriveIKGoal_ChainNames), + UEMannequinToMixamo_ChainNamesMapping.MapNames(UEMannequin_OneToOneFKRotationMode_ChainNames) + ); + + // Set-up the translation retargeting modes, to avoid artifacts when retargeting the animations. + SetupTranslationRetargetingModes(Skeleton); + // Retarget the base pose of the Mixamo skeletal meshes to match the "UE4_Mannequin_Skeleton" one. + const FStaticNamesMapper & UEMannequinToMixamo_BoneNamesMapping = SelectBySkeletonType(ReferenceSkeletonType, UE4MannequinToMixamo_BoneNamesMapping, UE5MannequinToMixamo_BoneNamesMapping); + RetargetBasePose( + SkeletalMeshes, + ReferenceSkeleton, + Mixamo_PreserveComponentSpacePose_BoneNames, + UEMannequinToMixamo_BoneNamesMapping.GetInverseMapper(), + Mixamo_ParentChildBoneNamesToBypassOneChildConstraint, + /*bApplyPoseToRetargetBasePose=*/true, + UIKRetargeterController::GetController(IKRetargeter_UEMannequinToMixamo) + ); + + // = Setup the Mixamo to UE Mannequin retargeting. + + // Get all USkeletalMesh assets using ReferenceSkeleton (i.e. the UE Mannequin skeleton). + TArray UEMannequinSkeletalMeshes; + GetAllSkeletalMeshesUsingSkeleton(ReferenceSkeleton, UEMannequinSkeletalMeshes); + +#ifndef MAR_IGNORE_UE5_MANNEQUIN + // In case Skeleton is a MetaHuman skeleton, remove all the skeletal meshes + // but "/Game/MetaHumans/Common/Female/Medium/NormalWeight/Body/f_med_nrw_body'. + // + // This because the "f_med_nrw_body" skeletal mesh is the only one that creates + // correct retargeted animations for all types of MetaHuman characters, also if + // the target MetaHuman character is using a different configuration (!) (e.g. + // Male-Tall-OverWeight, not sure how this works behind the scenes at the moment). + // When try using a different skeletal mesh, also if matching the target character + // configuration, the final animated character has evident rigging problems. + // Forcing the user with the "f_med_nrw_body" option only, guarantees correct results + // (provided the user preserves the corresponding preview skeletal mesh configured + // by the plugin). + if (ReferenceSkeletonType == ETargetSkeletonType::ST_UE5_MANNEQUIN) + { + UEMannequinSkeletalMeshes = UEMannequinSkeletalMeshes.FilterByPredicate([](USkeletalMesh* S) { + const FString PathName = S->GetPathName(); + return + !PathName.StartsWith("/Game/MetaHumans/", ESearchCase::IgnoreCase) + || FSoftObjectPath(PathName) == kMetaHumanDefaultSkeletalMesh_ObjectPath; + }); + } +#endif // MAR_IGNORE_UE5_MANNEQUIN + + // Create the IKRetarget asset to retarget from Mixamo to the UE Mannequin. + UIKRetargeter* IKRetargeter_MixamoToUEMannequin = CreateIKRetargeter( + SkeletonBasePackagePath, + GetRetargeterName(Skeleton, ReferenceSkeleton), + MixamoRig, + UEMannequinRig, + UEMannequinToMixamo_ChainNamesMapping, + UEMannequin_SkipChains_ChainNames, + UEMannequin_DriveIKGoal_ChainNames, + UEMannequin_OneToOneFKRotationMode_ChainNames + ); + + // Retarget the base pose of the UE Mannequin skeletal meshes to match the Mixamo skeleton one. + // Only in the IK Retargeter asset, do not change the UE Mannueqin Skeletal Meshes. + const TArray> & UEMannequin_ParentChildBoneNamesToBypassOneChildConstraint = SelectBySkeletonType(ReferenceSkeletonType, UE4Mannequin_ParentChildBoneNamesToBypassOneChildConstraint, UE5Mannequin_ParentChildBoneNamesToBypassOneChildConstraint); + RetargetBasePose( + UEMannequinSkeletalMeshes, + Skeleton, + UEMannequin_PreserveComponentSpacePose_BoneNames, + UEMannequinToMixamo_BoneNamesMapping, + UEMannequin_ParentChildBoneNamesToBypassOneChildConstraint, + /*bApplyPoseToRetargetBasePose=*/false, + UIKRetargeterController::GetController(IKRetargeter_MixamoToUEMannequin) + ); + + /* + // Open a content browser showing the specified assets (technically all the content of the directories containing them...). + + TArray ObjectsToSync; + ObjectsToSync.Add(IKRetargeter_UEMannequinToMixamo); + ObjectsToSync.Add(IKRetargeter_MixamoToUEMannequin); + ObjectsToSync.Add(MixamoRig); + ObjectsToSync.Add(UEMannequinRig); + GEditor->SyncBrowserToObjects(ObjectsToSync); + */ + + FMessageLog("LogMixamoToolkit").Info(FText::FromString(FString::Format(TEXT("Mixamo skeleton '{0}' retargeted successfully."), + { *Skeleton->GetName() }))); +} + + + +/// Return true if Skeleton has a bone named "root" and it's not at position 0; return false otherwise. +bool FMixamoSkeletonRetargeter::HasFakeRootBone(const USkeleton* Skeleton) const +{ + check(Skeleton != nullptr); + const int32 rootBoneIndex = Skeleton->GetReferenceSkeleton().FindBoneIndex(RootBoneName); + return rootBoneIndex != INDEX_NONE && rootBoneIndex != 0; +} + + + +/// Add the "root" bone to Skeleton and all its SkeletalMeshes. +void FMixamoSkeletonRetargeter::AddRootBone(USkeleton* Skeleton, TArray SkeletalMeshes) const +{ + // Skip if the mesh has already a bone named "root". + if (Skeleton->GetReferenceSkeleton().FindBoneIndex(RootBoneName) != INDEX_NONE) + { + return; + } + + //=== Add the root bone to all the Skeletal Meshes using Skeleton. + // We'll have to fix the Skeletal Meshes to account for the added root bone. + + // When going out of scope, it'll re-register components with the scene. + TComponentReregisterContext ReregisterContext; + + // Add the root bone to *all* skeletal meshes in SkeletalMeshes. + for (int iMesh = 0; iMesh < SkeletalMeshes.Num(); ++ iMesh) + { + USkeletalMesh * SkeletonMesh = SkeletalMeshes[iMesh]; + ensure(SkeletonMesh != nullptr); // @TODO: manage the nullptr case. + check(SkeletonMesh->GetSkeleton() == Skeleton); + + SkeletonMesh->Modify(); + + SkeletonMesh->ReleaseResources(); + SkeletonMesh->ReleaseResourcesFence.Wait(); + + // Add the root bone to the skeletal mesh's reference skeleton. + AddRootBone(SkeletonMesh->GetSkeleton(), &SkeletonMesh->GetRefSkeleton()); + // Fix-up bone transforms and reset RetargetBasePose. + SkeletonMesh->GetRetargetBasePose().Empty(); + SkeletonMesh->CalculateInvRefMatrices(); // @BUG: UE4 Undo system fails to undo the CalculateInvRefMatrices() effect. + + // As we added a new parent bone, fix "old" Skeletal Mesh indices. + uint32 LODIndex = 0; + for (FSkeletalMeshLODModel & LODModel : SkeletonMesh->GetImportedModel()->LODModels) + { + // == Fix the list of bones used by LODModel. + + // Increase old ActiveBoneIndices by 1, to compensate the new root bone. + for (FBoneIndexType & i : LODModel.ActiveBoneIndices) + { + ++i; + } + // Add the new root bone to the ActiveBoneIndices. + LODModel.ActiveBoneIndices.Insert(0, 0); + + // Increase old RequiredBones by 1, to compensate the new root bone. + for (FBoneIndexType & i : LODModel.RequiredBones) + { + ++i; + } + // Add the new root bone to the RequiredBones. + LODModel.RequiredBones.Insert(0, 0); + + // Updated the bone references used by the SkinWeightProfiles + for (auto & Kvp : LODModel.SkinWeightProfiles) + { + FImportedSkinWeightProfileData & SkinWeightProfile = LODModel.SkinWeightProfiles.FindChecked(Kvp.Key); + + // Increase old InfluenceBones by 1, to compensate the new root bone. + for (FRawSkinWeight & w : SkinWeightProfile.SkinWeights) + { + for (int i = 0; i < MAX_TOTAL_INFLUENCES; ++ i) + { + if (w.InfluenceWeights[i] > 0) + { + ++ w.InfluenceBones[i]; + } + } + } + + // Increase old BoneIndex by 1, to compensate the new root bone. + for (SkeletalMeshImportData::FVertInfluence & v: SkinWeightProfile.SourceModelInfluences) + { + if (v.Weight > 0) + { + ++ v.BoneIndex; + } + } + } + + // == Fix the mesh LOD sections. + + // Since UE4.24, newly imported Skeletal Mesh asset (UASSET) are serialized with additional data + // and are processed differently. On the post-edit change of the asset, the editor automatically + // re-builds all the sections starting from the stored raw mesh, if available. + // This is made to properly re-apply the reduction settings after changes. + // In this case, we must update the bones in the raw mesh and the editor will rebuild LODModel.Sections. + if (SkeletonMesh->IsLODImportedDataBuildAvailable(LODIndex) && !SkeletonMesh->IsLODImportedDataEmpty(LODIndex)) + { + FSkeletalMeshImportData RawMesh; + SkeletonMesh->LoadLODImportedData(LODIndex, RawMesh); + + // Increase old ParentIndex by 1, to compensate the new root bone. + int32 NumRootChildren = 0; + for (SkeletalMeshImportData::FBone & b : RawMesh.RefBonesBinary) + { + if (b.ParentIndex == INDEX_NONE) + { + NumRootChildren += b.NumChildren; + } + ++ b.ParentIndex; + } + // Add the new root bone to the RefBonesBinary. + check(NumRootChildren > 0); + const SkeletalMeshImportData::FJointPos NewRootPos = { FTransform3f::Identity, 1.f, 100.f, 100.f, 100.f }; + const SkeletalMeshImportData::FBone NewRoot = { RootBoneName.ToString(), 0, NumRootChildren, INDEX_NONE, NewRootPos }; + RawMesh.RefBonesBinary.Insert(NewRoot, 0); + + // Increase old BoneIndex by 1, to compensate the new root bone. + // Influences stores the pairs (vertex, bone), no need to add new items. + for (SkeletalMeshImportData::FRawBoneInfluence & b : RawMesh.Influences) + { + ++ b.BoneIndex; + } + + if (RawMesh.MorphTargets.Num() > 0) + { + FMessageLog("LogMixamoToolkit").Warning(FText::FromString(TEXT("MorphTargets are not supported."))); + } + + if (RawMesh.AlternateInfluences.Num() > 0) + { + FMessageLog("LogMixamoToolkit").Warning(FText::FromString(TEXT("AlternateInfluences are not supported."))); + } + + SkeletonMesh->SaveLODImportedData(LODIndex, RawMesh); + } + else + { + // For Skeletal Mesh assets (UASSET) using a pre-UE4.24 format (or missing the raw mesh data), + // we must manually update the LODModel.Sections to keep them synchronized with the new added root bone. + for (FSkelMeshSection & LODSection : LODModel.Sections) + { + // Increase old BoneMap indices by 1, to compensate the new root bone. + for (FBoneIndexType & i : LODSection.BoneMap) + { + ++i; + } + // No need to add the new root bone to BoneMap, as no vertices would use it. + // + // No need to update LODSection.SoftVertices[] items as FSoftSkinVertex::InfluenceBones + // contains indices over LODSection.BoneMap, that does't changed items positions. + } + } + + ++LODIndex; + } + + SkeletonMesh->PostEditChange(); + SkeletonMesh->InitResources(); + + // Use the modified skeletal mesh to recreate the Skeleton bones structure, so it'll contains also the new root bone. + // NOTE: this would invalidate the animations. + Skeleton->Modify(); + if (iMesh == 0) + { + // Use the first mesh to re-create the base bone tree... + Skeleton->RecreateBoneTree(SkeletonMesh); + } + else + { + // ...and then merge into Skeleton any new bone from SkeletonMesh. + Skeleton->MergeAllBonesToBoneTree(SkeletonMesh); + } + } +} + + + +/** +Add the "root" bone to a Skeletal Mesh's Reference Skeleton (RefSkeleton). +RefSkeleton must be based on Skeleton. +*/ +void FMixamoSkeletonRetargeter::AddRootBone(const USkeleton * Skeleton, FReferenceSkeleton * RefSkeleton) const +{ + check(Skeleton != nullptr); + check(RefSkeleton != nullptr); + checkf(RefSkeleton->FindBoneIndex(RootBoneName) == INDEX_NONE, TEXT("The reference skeleton has already a \"root\" bone.")); + + //=== Create a new FReferenceSkeleton with the root bone added. + FReferenceSkeleton NewRefSkeleton; + { + // Destructor rebuilds the ref-skeleton. + FReferenceSkeletonModifier RefSkeletonModifier(NewRefSkeleton, Skeleton); + + // Add the new root bone. + const FMeshBoneInfo Root(RootBoneName, RootBoneName.ToString(), INDEX_NONE); + RefSkeletonModifier.Add(Root, FTransform::Identity); + + // Copy and update existing bones indexes to get rid of the added root bone. + for (int32 i = 0; i < RefSkeleton->GetRawBoneNum(); ++i) + { + FMeshBoneInfo info = RefSkeleton->GetRawRefBoneInfo()[i]; + info.ParentIndex += 1; + const FTransform & pose = RefSkeleton->GetRawRefBonePose()[i]; + RefSkeletonModifier.Add(info, pose); + } + } + + // Set the new Reference Skeleton. + *RefSkeleton = NewRefSkeleton; +} + + + +/** +Setup the "Translation Retargeting" options for Skeleton (that is expected to be a Mixamo skeleton). + +This options are used by Unreal Engine to retarget animations using Skeleton +(and NOT to retarget animations using a different skeleton asset, this is done considering the retargeting pose instead). +The reason is that skeletal meshes using the same skeleton can have different sizes and proportions, +these options allow Unreal Engine to adapt an animation authored for a specific skeletal mesh to +a skeletal mesh with different proportions (but based on the same skeleton). + +See: +- https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimationRetargeting/index.html#settingupretargeting +- https://docs.unrealengine.com/latest/INT/Engine/Animation/RetargetingDifferentSkeletons/#retargetingadjustments +- https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimHowTo/Retargeting/index.html#retargetingusingthesameskeleton +*/ +void FMixamoSkeletonRetargeter::SetupTranslationRetargetingModes(USkeleton* Skeleton) const +{ + check(Skeleton != nullptr); + + const FReferenceSkeleton & RefSkeleton = Skeleton->GetReferenceSkeleton(); + Skeleton->Modify(); + + // Convert all bones, starting from the root one, to "Skeleton". + // This will ensure that all bones use the skeleton's static translation. + const int32 RootIndex = 0; +#ifndef MAR_ADDROOTBONE_DISABLE_ + checkf(RefSkeleton.FindBoneIndex(RootBoneName) == RootIndex, TEXT("Root bone at index 0")); +#endif + Skeleton->SetBoneTranslationRetargetingMode(RootIndex, EBoneTranslationRetargetingMode::Skeleton, true); + // Set the Pelvis bone (in Mixamo it's called "Hips") to AnimationScaled. + // This will make sure that the bone sits at the right height and is still animated. + const int32 PelvisIndex = RefSkeleton.FindBoneIndex(TEXT("Hips")); + if (PelvisIndex != INDEX_NONE) + { + Skeleton->SetBoneTranslationRetargetingMode(PelvisIndex, EBoneTranslationRetargetingMode::AnimationScaled); + } + // Find the Root bone, any IK bones, any Weapon bones you may be using or other marker-style bones and set them to Animation. + // This will make sure that bone's translation comes from the animation data itself and is unchanged. + // @TODO: do it for IK bones. + Skeleton->SetBoneTranslationRetargetingMode(RootIndex, EBoneTranslationRetargetingMode::Animation); +} + + + +/** +Configure the "retarget pose" of SkeletalMeshes to match the "reference pose" of ReferenceSkeleton. + +This is the pose needed by Unreal Engine to properly retarget animations involving different skeletons. +Animations are handled as additive bone transformations respect to the base pose of the skeletal mesh +for which they have been authored. + +The new "retarget base pose" is then stored/applied accordingly to the inputs. + +@param SkeletalMeshes Skeletal Meshes for which a "retarget pose" must be configured. +@param ReferenceSkeleton The skeleton with the "reference pose" that must be matched. + Here we use the term "reference" to indicate the skeleton we want to match, + do not confuse it with the "reference skeleton" term used by Unreal Engine to indicate the actual + and concrete skeleton used by a Skeletal Mesh or Skeleton asset. +@param PreserveCSBonesNames The bone names, in SkeletalMeshes's reference skeletons, that must preserve their Component Space transform. +@param ParentChildBoneNamesToBypassOneChildConstraint A set of parent-child bone names (in SkeletalMeshes's reference skeletons) that must be forcefully oriented regardless of + the children number of the parent bone. +@param EditToReference_BoneNamesMapping Mapping of bone names from the edited skeleton (ie. from SkeletalMeshes's reference skeletons) to + ReferenceSkeleton +@param bApplyPoseToRetargetBasePose If true, the computed "retarget pose" is applied to the "Retarget Base Pose" of the processed Skeletal Mesh. +@param Controller The IK Retargeter Controller to use to store the computed "retarget poses" in the processed Skeletal Mesh. Can be nullptr. +*/ +void FMixamoSkeletonRetargeter::RetargetBasePose( + TArray SkeletalMeshes, + const USkeleton * ReferenceSkeleton, + const TArray& PreserveCSBonesNames, + const FStaticNamesMapper & EditToReference_BoneNamesMapping, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + bool bApplyPoseToRetargetBasePose, + UIKRetargeterController* Controller +) const +{ + check(ReferenceSkeleton != nullptr); + check(Controller); + checkf(bApplyPoseToRetargetBasePose || Controller != nullptr, TEXT("Computed retarget pose must be saved/applied somewhere")); + + // @NOTE: UE4 mannequin skeleton must have same pose & proportions as of its skeletal mesh. + FSkeletonPoser poser(ReferenceSkeleton, ReferenceSkeleton->GetReferenceSkeleton().GetRefBonePose()); + + // Retarget all Skeletal Meshes using Skeleton. + for (USkeletalMesh * Mesh : SkeletalMeshes) + { + Controller->AddRetargetPose(Mesh->GetFName(), nullptr, ERetargetSourceOrTarget::Target); + + // Some of Mixamo's bones need a different rotation respect to UE4 mannequin reference pose. + // An analytics solution would be preferred, but (for now) preserving the CS pose of their + // children bones works quite well. + TArray MeshBonePose; + poser.PoseBasedOnMappedBoneNames(Mesh, + PreserveCSBonesNames, + EditToReference_BoneNamesMapping, + ParentChildBoneNamesToBypassOneChildConstraint, + MeshBonePose); + + if (bApplyPoseToRetargetBasePose) + { + FSkeletonPoser::ApplyPoseToRetargetBasePose(Mesh, MeshBonePose); + } + if (Controller != nullptr) + { + FSkeletonPoser::ApplyPoseToIKRetargetPose(Mesh, Controller, MeshBonePose); + } + + //@todo Add IK bones if possible. + } + + // Ensure the Controller is set with the pose of the rendered preview mesh. + if (Controller != nullptr) + { + if (USkeletalMesh* PreviewMesh = Controller->GetPreviewMesh(ERetargetSourceOrTarget::Target)) + { + Controller->SetCurrentRetargetPose(PreviewMesh->GetFName(), ERetargetSourceOrTarget::Target); + } + else + { + Controller->SetCurrentRetargetPose(Controller->GetAsset()->GetDefaultPoseName(), ERetargetSourceOrTarget::Target); + } + } +} + + + +/** +Ask to the user the Skeleton to use as "reference" for the retargeting. + +I.e. the one to which we want to retarget the currently processed skeleton. +*/ +USkeleton * FMixamoSkeletonRetargeter::AskUserForTargetSkeleton() const +{ + TSharedRef WidgetWindow = SNew(SWindow) + .Title(LOCTEXT("FMixamoSkeletonRetargeter_AskUserForTargetSkeleton_WindowTitle", "Select retargeting skeleton")) + .ClientSize(FVector2D(500, 600)) + .SupportsMinimize(false) + .SupportsMaximize(false) + .HasCloseButton(false); + + TSharedRef RiggedSkeletonPicker = SNew(SRiggedSkeletonPicker) + .Title(LOCTEXT("FMixamoSkeletonRetargeter_AskUserForTargetSkeleton_Title", "Select a Skeleton asset to use as retarget source.")) + .Description(LOCTEXT("FMixamoSkeletonRetargeter_AskUserForTargetSkeleton_Description", "For optimal results, it should be the standard Unreal Engine mannequin skeleton.")) + .OnShouldFilterAsset(SRiggedSkeletonPicker::FOnShouldFilterAsset::CreateRaw(this, &FMixamoSkeletonRetargeter::OnShouldFilterNonUEMannequinSkeletonAsset)); + + WidgetWindow->SetContent(RiggedSkeletonPicker); + GEditor->EditorAddModalWindow(WidgetWindow); + + return RiggedSkeletonPicker->GetSelectedSkeleton(); +} + + +bool FMixamoSkeletonRetargeter::AskUserOverridingAssetsConfirmation(const TArray& AssetsToOverwrite) const +{ + TSharedRef WidgetWindow = SNew(SWindow) + .Title(LOCTEXT("FMixamoSkeletonRetargeter_AskUserOverridingAssetsConfirmation_WindowTitle", "Overwrite confirmation")) + .ClientSize(FVector2D(400, 450)) + .SupportsMinimize(false) + .SupportsMaximize(false) + .HasCloseButton(false); + + TSharedRef ConfirmationDialog + = SNew(SOverridingAssetsConfirmationDialog) + .AssetsToOverwrite(AssetsToOverwrite); + + WidgetWindow->SetContent(ConfirmationDialog); + GEditor->EditorAddModalWindow(WidgetWindow); + + return ConfirmationDialog->HasConfirmed(); +} + + +/// Return the FAssetData of all the skeletal meshes based on Skeleton. +void FMixamoSkeletonRetargeter::GetAllSkeletalMeshesUsingSkeleton(const USkeleton * Skeleton, TArray & SkeletalMeshes) const +{ + SkeletalMeshes.Empty(); + + FARFilter Filter; + Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName()); + Filter.bRecursiveClasses = true; + const FString SkeletonString = FAssetData(Skeleton).GetExportTextName(); + Filter.TagsAndValues.Add(USkeletalMesh::GetSkeletonMemberName(), SkeletonString); + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().GetAssets(Filter, SkeletalMeshes); +} + + + +/** +Return the Skeletal Mesh assets of all the skeletal meshes based on Skeleton. + +This will load all the returned Skeletal Meshes. +*/ +void FMixamoSkeletonRetargeter::GetAllSkeletalMeshesUsingSkeleton(const USkeleton* Skeleton, TArray& SkeletalMeshes) const +{ + TArray Assets; + GetAllSkeletalMeshesUsingSkeleton(Skeleton, Assets); + SkeletalMeshes.Reset(Assets.Num()); + for (FAssetData& Asset : Assets) + { + // This will load the asset if needed. + SkeletalMeshes.Emplace(CastChecked(Asset.GetAsset())); + } +} + + + +/// If Skeleton doesn't already have a Preview Mesh, then set it to PreviewMesh. +void FMixamoSkeletonRetargeter::SetPreviewMesh(USkeleton * Skeleton, USkeletalMesh * PreviewMesh) const +{ + check(Skeleton != nullptr); + + if (Skeleton->GetPreviewMesh() == nullptr && PreviewMesh != nullptr) + Skeleton->SetPreviewMesh(PreviewMesh); +} + + + +void FMixamoSkeletonRetargeter::EnumerateAssetsToOverwrite(const USkeleton* Skeleton, const USkeleton* ReferenceSkeleton, TArray& AssetsToOverride) const +{ + auto AddIfExists = [&AssetsToOverride](const FString& PackagePath, const FString& AssetName) + { + const FString LongPackageName = PackagePath / AssetName; + + UObject* Package = StaticFindObject(UObject::StaticClass(), nullptr, *LongPackageName); + + if (!Package) + { + Package = LoadPackage(nullptr, *LongPackageName, LOAD_NoWarn); + } + + if (Package) + { + if (UObject* Obj = FindObject(Package, *AssetName)) + { + AssetsToOverride.AddUnique(Obj); + } + } + }; + + { + const FString PackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + AddIfExists(PackagePath, GetRigName(Skeleton)); + } + { + const FString PackagePath = FPackageName::GetLongPackagePath(ReferenceSkeleton->GetPackage()->GetName()); + AddIfExists(PackagePath, GetRigName(ReferenceSkeleton)); + } + { + const FString PackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + AddIfExists(PackagePath, GetRetargeterName(Skeleton, ReferenceSkeleton)); + } + { + const FString PackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + AddIfExists(PackagePath, GetRetargeterName(ReferenceSkeleton, Skeleton)); + } +} + + + +/** +Create an IK Rig asset for Skeleton (it's Preview Mesh). +*/ +UIKRigDefinition* FMixamoSkeletonRetargeter::CreateIKRig( + const FString & PackagePath, + const FString & AssetName, + const USkeleton* Skeleton +) const +{ + check(Skeleton); + + const FString LongPackageName = PackagePath / AssetName; + UPackage* Package = UPackageTools::FindOrCreatePackageForAssetType(FName(*LongPackageName), UIKRigDefinition::StaticClass()); + check(Package); + + UIKRigDefinition* IKRig = NewObject(Package, FName(*AssetName), RF_Standalone | RF_Public | RF_Transactional); + + // Notify the asset registry + FAssetRegistryModule::AssetCreated(IKRig); + // Mark the package dirty... + Package->MarkPackageDirty(); + + // imports the skeleton data into the IK Rig + UIKRigController* Controller = UIKRigController::GetIKRigController(IKRig); + USkeletalMesh* SM = Skeleton->GetPreviewMesh(); + check(SM != nullptr); + Controller->SetSkeletalMesh(SM); + + return IKRig; +} + + + +/** +Create an IK Rig asset for a Mixamo Skeleton (it's Preview Mesh). +*/ +UIKRigDefinition* FMixamoSkeletonRetargeter::CreateMixamoIKRig(const USkeleton* Skeleton) const +{ + check(Skeleton); + + const FString PackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + UIKRigDefinition* IKRig = CreateIKRig(PackagePath, GetRigName(Skeleton), Skeleton); + + // imports the skeleton data into the IK Rig + UIKRigController* Controller = UIKRigController::GetIKRigController(IKRig); + USkeletalMesh* SM = Skeleton->GetPreviewMesh(); + check(SM != nullptr); + Controller->SetSkeletalMesh(SM); + + static const FName RetargetRootBone(TEXT("Hips")); + +#ifndef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + Controller->AddRetargetChain(FBoneChain(TEXT("Root"), TEXT("root"), TEXT("root"))); + Controller->AddRetargetChain(FBoneChain(TEXT("Spine"), TEXT("Spine"), TEXT("Spine2"))); + Controller->AddRetargetChain(FBoneChain(TEXT("Head"), TEXT("Neck"), TEXT("head"))); + static const FName LeftClavicleChainName(TEXT("LeftClavicle")); + Controller->AddRetargetChain(FBoneChain(LeftClavicleChainName, TEXT("LeftShoulder"), TEXT("LeftShoulder"))); + static const FName LeftArmChainName(TEXT("LeftArm")); + static const FName LeftHandBoneName(TEXT("LeftHand")); + Controller->AddRetargetChain(FBoneChain(LeftArmChainName, TEXT("LeftArm"), LeftHandBoneName)); + static const FName RightClavicleChainName(TEXT("RightClavicle")); + Controller->AddRetargetChain(FBoneChain(RightClavicleChainName, TEXT("RightShoulder"), TEXT("RightShoulder"))); + static const FName RightArmChainName(TEXT("RightArm")); + static const FName RightHandBoneName(TEXT("RightHand")); + Controller->AddRetargetChain(FBoneChain(RightArmChainName, TEXT("RightArm"), RightHandBoneName)); + static const FName LeftLegChainName(TEXT("LeftLeg")); + static const FName LeftToeBaseBoneName(TEXT("LeftToeBase")); + Controller->AddRetargetChain(FBoneChain(LeftLegChainName, TEXT("LeftUpLeg"), LeftToeBaseBoneName)); + static const FName RightLegChainName(TEXT("RightLeg")); + static const FName RightToeBaseBoneName(TEXT("RightToeBase")); + Controller->AddRetargetChain(FBoneChain(RightLegChainName, TEXT("RightUpLeg"), RightToeBaseBoneName)); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftIndex"), TEXT("LeftHandIndex1"), TEXT("LeftHandIndex3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightIndex"), TEXT("RightHandIndex1"), TEXT("RightHandIndex3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftMiddle"), TEXT("LeftHandMiddle1"), TEXT("LeftHandMiddle3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightMiddle"), TEXT("RightHandMiddle1"), TEXT("RightHandMiddle3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftPinky"), TEXT("LeftHandPinky1"), TEXT("LeftHandPinky3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightPinky"), TEXT("RightHandPinky1"), TEXT("RightHandPinky3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftRing"), TEXT("LeftHandRing1"), TEXT("LeftHandRing3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightRing"), TEXT("RightHandRing1"), TEXT("RightHandRing3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftThumb"), TEXT("LeftHandThumb1"), TEXT("LeftHandThumb3"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightThumb"), TEXT("RightHandThumb1"), TEXT("RightHandThumb3"))); + +#ifndef MAR_IKRETARGETER_IKSOLVERS_DISABLE_ + + const int32 SolverIndex = Controller->AddSolver(UIKRigPBIKSolver::StaticClass()); + UIKRigPBIKSolver* Solver = CastChecked(Controller->GetSolver(SolverIndex)); + Solver->SetRootBone(RetargetRootBone); + Solver->RootBehavior = EPBIKRootBehavior::PinToInput; + + // Hips bone settings + static const FName HipsBoneName(TEXT("Hips")); + Controller->AddBoneSetting(HipsBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* HipsBoneSettings = Cast(Controller->GetSettingsForBone(HipsBoneName, SolverIndex))) + { + HipsBoneSettings->RotationStiffness = 0.99f; + } + + // Spine bone settings + static const FName SpineBoneName(TEXT("Spine")); + Controller->AddBoneSetting(SpineBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* SpineBoneSettings = Cast(Controller->GetSettingsForBone(SpineBoneName, SolverIndex))) + { + SpineBoneSettings->RotationStiffness = 0.7f; + } + + // Spine1 bone settings + static const FName Spine1BoneName(TEXT("Spine1")); + Controller->AddBoneSetting(Spine1BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine1BoneSettings = Cast(Controller->GetSettingsForBone(Spine1BoneName, SolverIndex))) + { + Spine1BoneSettings->RotationStiffness = 0.8f; + } + + // Spine2 bone settings + static const FName Spine2BoneName(TEXT("Spine2")); + Controller->AddBoneSetting(Spine2BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine2BoneSettings = Cast(Controller->GetSettingsForBone(Spine2BoneName, SolverIndex))) + { + Spine2BoneSettings->RotationStiffness = 0.95f; + } + + // Left Shoulder bone settings + static const FName LeftShoulderBoneName(TEXT("LeftShoulder")); + Controller->AddBoneSetting(LeftShoulderBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftShoulderBoneSettings = Cast(Controller->GetSettingsForBone(LeftShoulderBoneName, SolverIndex))) + { + LeftShoulderBoneSettings->RotationStiffness = 0.99f; + } + + // Left Hand goal + static const FName LeftHandGoalName(TEXT("LeftHand_Goal")); + if (UIKRigEffectorGoal* LeftHandGoal = Controller->AddNewGoal(LeftHandGoalName, LeftHandBoneName)) + { + LeftHandGoal->bExposePosition = true; + LeftHandGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(LeftArmChainName, LeftHandGoalName); + if (UIKRig_FBIKEffector* LeftHandGoalSettings = Cast(Controller->GetGoalSettingsForSolver(LeftHandGoalName, SolverIndex))) + { + LeftHandGoalSettings->PullChainAlpha = 0.f; + } + } + + // Right Shoulder bone settings + static const FName RightShoulderBoneName(TEXT("RightShoulder")); + Controller->AddBoneSetting(RightShoulderBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightShoulderBoneSettings = Cast(Controller->GetSettingsForBone(RightShoulderBoneName, SolverIndex))) + { + RightShoulderBoneSettings->RotationStiffness = 0.99f; + } + + // Right Hand goal + static const FName RightHandGoalName(TEXT("RightHand_Goal")); + if (UIKRigEffectorGoal* RightHandGoal = Controller->AddNewGoal(RightHandGoalName, RightHandBoneName)) + { + RightHandGoal->bExposePosition = true; + RightHandGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(RightArmChainName, RightHandGoalName); + if (UIKRig_FBIKEffector* RightHandGoalSettings = Cast(Controller->GetGoalSettingsForSolver(RightHandGoalName, SolverIndex))) + { + RightHandGoalSettings->PullChainAlpha = 0.f; + } + } + + // Left forearm settings + static const FName LeftForeArmBoneName(TEXT("LeftForeArm")); + Controller->AddBoneSetting(LeftForeArmBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftForeArmBoneSettings = Cast(Controller->GetSettingsForBone(LeftForeArmBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + LeftForeArmBoneSettings, + FName("LeftHand"), + { 0, -90, 0 }, + FVector::UpVector + ); + } + + // Right forearm settings + static const FName RightForeArmBoneName(TEXT("RightForeArm")); + Controller->AddBoneSetting(RightForeArmBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightForeArmBoneSettings = Cast(Controller->GetSettingsForBone(RightForeArmBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + RightForeArmBoneSettings, + FName("RightHand"), + { 0, 90, 0 }, + FVector::UpVector + ); + } + + // Left Up Leg bone settings + static const FName LeftUpLegBoneName(TEXT("LeftUpLeg")); + Controller->AddBoneSetting(LeftUpLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftUpLegBoneSettings = Cast(Controller->GetSettingsForBone(LeftUpLegBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + LeftUpLegBoneSettings, + FName("LeftLeg"), + { 0, -90, 0 }, + FVector::ForwardVector + ); + } + + // Left Leg bone settings + static const FName LeftLegBoneName(TEXT("LeftLeg")); + Controller->AddBoneSetting(LeftLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftLegBoneSettings = Cast(Controller->GetSettingsForBone(LeftLegBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + LeftLegBoneSettings, + FName("LeftFoot"), + { 0, 90, 0 }, + FVector::ForwardVector + ); + } + + // Left Foot goal + static const FName LeftFootGoalName(TEXT("LeftFoot_Goal")); + if (UIKRigEffectorGoal* LeftFootGoal = Controller->AddNewGoal(LeftFootGoalName, LeftToeBaseBoneName)) + { + LeftFootGoal->bExposePosition = true; + LeftFootGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(LeftLegChainName, LeftFootGoalName); + } + + // Right Up Leg bone settings + static const FName RightUpLegBoneName(TEXT("RightUpLeg")); + Controller->AddBoneSetting(RightUpLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightUpLegBoneSettings = Cast(Controller->GetSettingsForBone(RightUpLegBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + RightUpLegBoneSettings, + FName("RightLeg"), + { 0, -90, 0 }, + FVector::ForwardVector + ); + } + + // Right Leg bone settings + static const FName RightLegBoneName(TEXT("RightLeg")); + Controller->AddBoneSetting(RightLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightLegBoneSettings = Cast(Controller->GetSettingsForBone(RightLegBoneName, SolverIndex))) + { + ConfigureBonePreferredAnglesLocalToBS( + Skeleton, + RightLegBoneSettings, + FName("RightFoot"), + { 0, 90, 0 }, + FVector::ForwardVector + ); + } + + // Right Foot goal + static const FName RightFootGoalName(TEXT("RightFoot_Goal")); + if (UIKRigEffectorGoal* RightFootGoal = Controller->AddNewGoal(RightFootGoalName, RightToeBaseBoneName)) + { + RightFootGoal->bExposePosition = true; + RightFootGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(RightLegChainName, RightFootGoalName); + } + +#endif // MAR_IKRETARGETER_IKSOLVERS_DISABLE_ + +#else // MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + TArray BoneNames; + UE4MannequinToMixamo_BoneNamesMapping.GetDestination(BoneNames); // NOTE: it's ok as long as the UE5 version has the same destination bone names. + for (const FName& BoneName : BoneNames) + { + Controller->AddRetargetChain(FBoneChain(/*ChainName=*/BoneName, /*StartBoneName=*/BoneName, /*EndBoneName=*/BoneName)); + } + +#endif // MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + Controller->SetRetargetRoot(RetargetRootBone); + + return IKRig; +} + + + +/** +Create an IK Rig asset for a UE Mannequin Skeleton (it's Preview Mesh). +*/ +UIKRigDefinition* FMixamoSkeletonRetargeter::CreateUEMannequinIKRig( + const USkeleton* Skeleton, + ETargetSkeletonType SkeletonType +) const +{ + check(Skeleton != nullptr); + + const FString PackagePath = FPackageName::GetLongPackagePath(Skeleton->GetPackage()->GetName()); + UIKRigDefinition* IKRig = CreateIKRig(PackagePath, GetRigName(Skeleton), Skeleton); + + // imports the skeleton data into the IK Rig + UIKRigController* Controller = UIKRigController::GetIKRigController(IKRig); + USkeletalMesh* SM = Skeleton->GetPreviewMesh(); + check(SM != nullptr); + Controller->SetSkeletalMesh(SM); + + static const FName RetargetRootBone(TEXT("pelvis")); + + const bool bIsUE5Skeleton = SkeletonType == ETargetSkeletonType::ST_UE5_MANNEQUIN; + +#ifndef MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + Controller->AddRetargetChain(FBoneChain(TEXT("Root"), TEXT("root"), TEXT("root"))); + Controller->AddRetargetChain(FBoneChain(TEXT("Spine"), TEXT("spine_01"), SelectBySkeletonType(SkeletonType, TEXT("spine_03"), TEXT("spine_05")))); + Controller->AddRetargetChain(FBoneChain(TEXT("Head"), TEXT("neck_01"), TEXT("head"))); + static const FName LeftClavicleChainName(TEXT("LeftClavicle")); + Controller->AddRetargetChain(FBoneChain(LeftClavicleChainName, TEXT("clavicle_l"), TEXT("clavicle_l"))); + static const FName LeftArmChainName(TEXT("LeftArm")); + static const FName LeftHandBoneName(TEXT("hand_l")); + Controller->AddRetargetChain(FBoneChain(LeftArmChainName, TEXT("upperarm_l"), LeftHandBoneName)); + static const FName RightClavicleChainName(TEXT("RightClavicle")); + Controller->AddRetargetChain(FBoneChain(RightClavicleChainName, TEXT("clavicle_r"), TEXT("clavicle_r"))); + static const FName RightArmChainName(TEXT("RightArm")); + static const FName RightHandBoneName(TEXT("hand_r")); + Controller->AddRetargetChain(FBoneChain(RightArmChainName, TEXT("upperarm_r"), RightHandBoneName)); + static const FName LeftLegChainName(TEXT("LeftLeg")); + static const FName LeftBallBoneName(TEXT("ball_l")); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftLeg"), TEXT("thigh_l"), LeftBallBoneName)); + static const FName RightLegChainName(TEXT("RightLeg")); + static const FName RightBallBoneName(TEXT("ball_r")); + Controller->AddRetargetChain(FBoneChain(RightLegChainName, TEXT("thigh_r"), RightBallBoneName)); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftIndex"), TEXT("index_01_l"), TEXT("index_03_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightIndex"), TEXT("index_01_r"), TEXT("index_03_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftMiddle"), TEXT("middle_01_l"), TEXT("middle_03_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightMiddle"), TEXT("middle_01_r"), TEXT("middle_03_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftPinky"), TEXT("pinky_01_l"), TEXT("pinky_03_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightPinky"), TEXT("pinky_01_r"), TEXT("pinky_03_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftRing"), TEXT("ring_01_l"), TEXT("ring_03_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightRing"), TEXT("ring_01_r"), TEXT("ring_03_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftThumb"), TEXT("thumb_01_l"), TEXT("thumb_03_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightThumb"), TEXT("thumb_01_r"), TEXT("thumb_03_r"))); + if (bIsUE5Skeleton) + { + // If we don't add them (also if apparently useless), the IK Retargeter editor (UE5.0.2) wrongly processes the + // descendant bones hierarchy (see Issue #863): the metacarpal bones are not drawn in the editor and + // children bones have a wrong trasformation applied, resulting in a wrong pose of the fingers. + Controller->AddRetargetChain(FBoneChain(TEXT("LeftIndexMetacarpal"), TEXT("index_metacarpal_l"), TEXT("index_metacarpal_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightIndexMetacarpal"), TEXT("index_metacarpal_r"), TEXT("index_metacarpal_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftMiddleMetacarpal"), TEXT("middle_metacarpal_l"), TEXT("middle_metacarpal_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightMiddleMetacarpal"), TEXT("middle_metacarpal_r"), TEXT("middle_metacarpal_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftPinkyMetacarpal"), TEXT("pinky_metacarpal_l"), TEXT("pinky_metacarpal_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightPinkyMetacarpal"), TEXT("pinky_metacarpal_r"), TEXT("pinky_metacarpal_r"))); + Controller->AddRetargetChain(FBoneChain(TEXT("LeftRingMetacarpal"), TEXT("ring_metacarpal_l"), TEXT("ring_metacarpal_l"))); + Controller->AddRetargetChain(FBoneChain(TEXT("RightRingMetacarpal"), TEXT("ring_metacarpal_r"), TEXT("ring_metacarpal_r"))); + } + // NOTE: for ST_UE5_MANNEQUIN are missing: all *Twist*, *IK chains. + +#ifndef MAR_IKRETARGETER_IKSOLVERS_DISABLE_ + + const int32 SolverIndex = Controller->AddSolver(UIKRigPBIKSolver::StaticClass()); + UIKRigPBIKSolver* Solver = CastChecked(Controller->GetSolver(SolverIndex)); + Solver->SetRootBone(RetargetRootBone); + Solver->RootBehavior = EPBIKRootBehavior::PinToInput; + + // Pelvis bone settings + static const FName PelvisBoneName(TEXT("pelvis")); + Controller->AddBoneSetting(PelvisBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* HipsBoneSettings = Cast(Controller->GetSettingsForBone(PelvisBoneName, SolverIndex))) + { + HipsBoneSettings->RotationStiffness = 1.0f; + } + + // Spine_01 bone settings + static const FName Spine1BoneName(TEXT("spine_01")); + Controller->AddBoneSetting(Spine1BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine1BoneSettings = Cast(Controller->GetSettingsForBone(Spine1BoneName, SolverIndex))) + { + Spine1BoneSettings->RotationStiffness = SelectBySkeletonType(SkeletonType, 0.784f, 0.896f); + } + + // Spine_02 bone settings + static const FName Spine2BoneName(TEXT("spine_02")); + Controller->AddBoneSetting(Spine2BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine2BoneSettings = Cast(Controller->GetSettingsForBone(Spine2BoneName, SolverIndex))) + { + Spine2BoneSettings->RotationStiffness = SelectBySkeletonType(SkeletonType, 0.928f, 0.936f); + } + + // Spine_03 bone settings + static const FName Spine3BoneName(TEXT("spine_03")); + Controller->AddBoneSetting(Spine3BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine3BoneSettings = Cast(Controller->GetSettingsForBone(Spine3BoneName, SolverIndex))) + { + Spine3BoneSettings->RotationStiffness = 0.936f; + } + + if (bIsUE5Skeleton) + { + // Spine_04 bone settings + static const FName Spine4BoneName(TEXT("spine_04")); + Controller->AddBoneSetting(Spine4BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine4BoneSettings = Cast(Controller->GetSettingsForBone(Spine4BoneName, SolverIndex))) + { + Spine4BoneSettings->RotationStiffness = 0.936f; + } + + // Spine_05 bone settings + static const FName Spine5BoneName(TEXT("spine_05")); + Controller->AddBoneSetting(Spine5BoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* Spine5BoneSettings = Cast(Controller->GetSettingsForBone(Spine5BoneName, SolverIndex))) + { + Spine5BoneSettings->RotationStiffness = 0.936f; + } + } + + // Clavicle Left bone settings + static const FName ClavicleLeftBoneName(TEXT("clavicle_l")); + Controller->AddBoneSetting(ClavicleLeftBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* ClavicleLeftBoneSettings = Cast(Controller->GetSettingsForBone(ClavicleLeftBoneName, SolverIndex))) + { + ClavicleLeftBoneSettings->RotationStiffness = 1.0f; + } + + // Left Lower arm bone settings + static const FName LowerArmLeftBoneName(TEXT("lowerarm_l")); + Controller->AddBoneSetting(LowerArmLeftBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LowerArmLeftBoneSettings = Cast(Controller->GetSettingsForBone(LowerArmLeftBoneName, SolverIndex))) + { + LowerArmLeftBoneSettings->bUsePreferredAngles = true; + LowerArmLeftBoneSettings->PreferredAngles = { 0, 0, 90 }; + } + + // Left Hand goal + static const FName LeftHandGoalName(TEXT("hand_l_Goal")); + if (UIKRigEffectorGoal* LeftHandGoal = Controller->AddNewGoal(LeftHandGoalName, LeftHandBoneName)) + { + LeftHandGoal->bExposePosition = true; + LeftHandGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(LeftArmChainName, LeftHandGoalName); + if (UIKRig_FBIKEffector* LeftHandGoalSettings = Cast(Controller->GetGoalSettingsForSolver(LeftHandGoalName, SolverIndex))) + { + LeftHandGoalSettings->PullChainAlpha = 0.f; + } + } + + // Clavicle Right bone settings + static const FName ClavicleRightBoneName(TEXT("clavicle_r")); + Controller->AddBoneSetting(ClavicleRightBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* ClavicleRightBoneSettings = Cast(Controller->GetSettingsForBone(ClavicleRightBoneName, SolverIndex))) + { + ClavicleRightBoneSettings->RotationStiffness = 1.0f; + } + + // Right Lower arm bone settings + static const FName LowerArmRightBoneName(TEXT("lowerarm_r")); + Controller->AddBoneSetting(LowerArmRightBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LowerArmRightBoneSettings = Cast(Controller->GetSettingsForBone(LowerArmRightBoneName, SolverIndex))) + { + LowerArmRightBoneSettings->bUsePreferredAngles = true; + LowerArmRightBoneSettings->PreferredAngles = { 0, 0, 90 }; + } + + // Right Hand goal + static const FName RightHandGoalName(TEXT("hand_r_Goal")); + if (UIKRigEffectorGoal* RightHandGoal = Controller->AddNewGoal(RightHandGoalName, RightHandBoneName)) + { + RightHandGoal->bExposePosition = true; + RightHandGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(RightArmChainName, RightHandGoalName); + if (UIKRig_FBIKEffector* RightHandGoalSettings = Cast(Controller->GetGoalSettingsForSolver(RightHandGoalName, SolverIndex))) + { + RightHandGoalSettings->PullChainAlpha = 0.f; + } + } + + // Left Leg bone settings + static const FName LeftLegBoneName(TEXT("calf_l")); + Controller->AddBoneSetting(LeftLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftLegBoneSettings = Cast(Controller->GetSettingsForBone(LeftLegBoneName, SolverIndex))) + { + LeftLegBoneSettings->bUsePreferredAngles = true; + LeftLegBoneSettings->PreferredAngles = { 0, 0, 90 }; + } + + // Left Foot goal + static const FName LeftFootGoalName(TEXT("foot_l_Goal")); + if (UIKRigEffectorGoal* LeftFootGoal = Controller->AddNewGoal(LeftFootGoalName, LeftBallBoneName)) + { + LeftFootGoal->bExposePosition = true; + LeftFootGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(LeftLegChainName, LeftFootGoalName); + } + + // Right Leg bone settings + static const FName RightLegBoneName(TEXT("calf_r")); + Controller->AddBoneSetting(RightLegBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightLegBoneSettings = Cast(Controller->GetSettingsForBone(RightLegBoneName, SolverIndex))) + { + RightLegBoneSettings->bUsePreferredAngles = true; + RightLegBoneSettings->PreferredAngles = { 0, 0, 90 }; + } + + // Right Foot goal + static const FName RightFootGoalName(TEXT("foot_r_Goal")); + if (UIKRigEffectorGoal* RightFootGoal = Controller->AddNewGoal(RightFootGoalName, RightBallBoneName)) + { + RightFootGoal->bExposePosition = true; + RightFootGoal->bExposeRotation = true; + Controller->SetRetargetChainGoal(RightLegChainName, RightFootGoalName); + } + + // Left Thigh bone settings + static const FName LeftThighBoneName(TEXT("thigh_l")); + Controller->AddBoneSetting(LeftThighBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* LeftThighBoneSettings = Cast(Controller->GetSettingsForBone(LeftThighBoneName, SolverIndex))) + { + LeftThighBoneSettings->bUsePreferredAngles = true; + LeftThighBoneSettings->PreferredAngles = { 0, 0, -90 }; + } + + // Right Thigh bone settings + static const FName RightThighBoneName(TEXT("thigh_r")); + Controller->AddBoneSetting(RightThighBoneName, SolverIndex); + if (UIKRig_PBIKBoneSettings* RightThighBoneSettings = Cast(Controller->GetSettingsForBone(RightThighBoneName, SolverIndex))) + { + RightThighBoneSettings->bUsePreferredAngles = true; + RightThighBoneSettings->PreferredAngles = { 0, 0, -90 }; + } + +#endif // MAR_IKRETARGETER_IKSOLVERS_DISABLE_ + +#else // MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + TArray BoneNames; + const FStaticNamesMapper& UEMannequinToMixamo_BoneNamesMapping = SelectBySkeletonType(SkeletonType, UE4MannequinToMixamo_BoneNamesMapping, UE5MannequinToMixamo_BoneNamesMapping); + UEMannequinToMixamo_BoneNamesMapping.GetSource(BoneNames); + for (const FName& BoneName : BoneNames) + { + Controller->AddRetargetChain(FBoneChain(/*ChainName=*/BoneName, /*StartBoneName=*/BoneName, /*EndBoneName=*/BoneName)); + } + +#endif // MAR_IKRETARGETER_ADVANCED_CHAINS_DISABLE_ + + Controller->SetRetargetRoot(RetargetRootBone); + + return IKRig; +} + + + +/** +Create an IK Retargeter asset from Source Rig to Target Rig. + +@param TargetToSource_ChainNamesMapping Mapper of chain names from the TargetRig to the SourceRig. +@param BoneChainsToSkip Set of IK Rig chain names (relative to TargetRig) for which a "retarget chain" must not be configured. +@param TargetBoneChainsDriveIKGoalToSource Set of IK Rig chain names (relative to TargetRig) for which a the "Drive IK Goal" must be configured. +*/ +UIKRetargeter* FMixamoSkeletonRetargeter::CreateIKRetargeter( + const FString & PackagePath, + const FString & AssetName, + UIKRigDefinition* SourceRig, + UIKRigDefinition* TargetRig, + const FStaticNamesMapper & TargetToSource_ChainNamesMapping, + const TArray & TargetBoneChainsToSkip, + const TArray & TargetBoneChainsDriveIKGoal, + const TArray& TargetBoneChainsOneToOneRotationMode +) const +{ + /* + // Using the FAssetToolsModule (add AssetTools to Build.cs and #include "AssetToolsModule.h"): + + FAssetToolsModule& AssetToolsModule = FAssetToolsModule::GetModule(); + FString UniquePackageName; + FString UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath / AssetName, TEXT(""), UniquePackageName, UniqueAssetName); + const FString UniquePackagePath = FPackageName::GetLongPackagePath(UniquePackageName); + + UIKRetargeter* Retargeter = CastChecked(AssetToolsModule.Get().CreateAsset(UniqueAssetName, UniquePackagePath, UIKRetargeter::StaticClass(), nullptr)); + */ + + const FString LongPackageName = PackagePath / AssetName; + UPackage* Package = UPackageTools::FindOrCreatePackageForAssetType(FName(*LongPackageName), UIKRetargeter::StaticClass()); + check(Package); + + UIKRetargeter* Retargeter = NewObject(Package, FName(*AssetName), RF_Standalone | RF_Public | RF_Transactional); + + // Notify the asset registry + FAssetRegistryModule::AssetCreated(Retargeter); + // Mark the package dirty... + Package->MarkPackageDirty(); + + UIKRetargeterController* Controller = UIKRetargeterController::GetController(Retargeter); + Controller->SetSourceIKRig(SourceRig); + + // Controller->SetTargetIKRig(TargetRig) is BUGGED, do not set the TargetIKRig! Set it with reflection. + FObjectPropertyBase* TargetIKRigProperty = CastFieldChecked( + UIKRetargeter::StaticClass()->FindPropertyByName(UIKRetargeter::GetTargetIKRigPropertyName())); + if (ensure(TargetIKRigProperty)) + { + void* ptr = TargetIKRigProperty->ContainerPtrToValuePtr(Retargeter); + check(ptr); + TargetIKRigProperty->SetObjectPropertyValue(ptr, TargetRig); + } + + Controller->CleanChainMapping(); + for (URetargetChainSettings* ChainMap : Controller->GetChainMappings()) + { + // Check if we need to explicitly skip an existing bone chain. + if (TargetBoneChainsToSkip.Contains(ChainMap->TargetChain)) + { + continue; + } + // Search the mapped bone name. + const FName SourceChainName = TargetToSource_ChainNamesMapping.MapName(ChainMap->TargetChain); + // Skip if the targte chain name is not mapped. + if (SourceChainName.IsNone()) + { + continue; + } + + // Add the Target->Source chain name association. + Controller->SetSourceChainForTargetChain(ChainMap, SourceChainName); + + //= Configure the ChainMap settings + + // this is needed for root motion + if (SourceChainName == FName("Root")) + { + ChainMap->Settings.FK.TranslationMode = ERetargetTranslationMode::GloballyScaled; + } + +#ifndef MAR_IKRETARGETER_IKSOLVERS_DISABLE_ + // Configure the DriveIKGoal setting. + ChainMap->Settings.IK.EnableIK = TargetBoneChainsDriveIKGoal.Contains(ChainMap->TargetChain); + + if (TargetBoneChainsOneToOneRotationMode.Contains(ChainMap->TargetChain)) + { + ChainMap->Settings.FK.RotationMode = ERetargetRotationMode::OneToOne; + } +#endif + } + + return Retargeter; +} + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.h new file mode 100644 index 00000000..872160c0 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoSkeletonRetargeter.h @@ -0,0 +1,105 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Engine/EngineTypes.h" + +#include "NamesMapper.h" + + + +class UIKRigDefinition; +class UIKRetargeter; +class USkeleton; +class USkeletalMesh; +struct FReferenceSkeleton; + + + +/** +Type of the target skeleton when retargeting from a Mixamo skeleton. + +At the moment we assume that ST_UE5_MANNEQUIN can be used also for the MetaHuman skeleton, +if needed we'll add a distinct ST_METAHUMAN value in future. +*/ +enum class ETargetSkeletonType +{ + ST_UNKNOWN = 0, + ST_UE4_MANNEQUIN, + ST_UE5_MANNEQUIN, + + ST_SIZE +}; + + + +/** +Manage the retargeting of a Mixamo skeleton. + +Further info: +- https://docs.unrealengine.com/latest/INT/Engine/Animation/Skeleton/ +- https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimationRetargeting/index.html +- https://docs.unrealengine.com/latest/INT/Engine/Animation/AnimHowTo/Retargeting/index.html +- https://docs.unrealengine.com/latest/INT/Engine/Animation/RetargetingDifferentSkeletons/ +*/ +class FMixamoSkeletonRetargeter +{ +public: + FMixamoSkeletonRetargeter(); + + void RetargetToUE4Mannequin(TArray Skeletons) const; + bool IsMixamoSkeleton(const USkeleton * Skeleton) const; + +private: + bool OnShouldFilterNonUEMannequinSkeletonAsset(const FAssetData& AssetData) const; + ETargetSkeletonType GetTargetSkeletonType(const USkeleton* Skeleton) const; + bool IsUEMannequinSkeleton(const USkeleton * Skeleton) const; + void Retarget(USkeleton* Skeleton, const USkeleton * ReferenceSkeleton, ETargetSkeletonType ReferenceSkeletonType) const; + bool HasFakeRootBone(const USkeleton* Skeleton) const; + void AddRootBone(USkeleton * Skeleton, TArray SkeletalMeshes) const; + void AddRootBone(const USkeleton * Skeleton, FReferenceSkeleton * RefSkeleton) const; + void SetupTranslationRetargetingModes(USkeleton* Skeleton) const; + void RetargetBasePose( + TArray SkeletalMeshes, + const USkeleton * ReferenceSkeleton, + const TArray& PreserveCSBonesNames, + const FStaticNamesMapper & EditToReference_BoneNamesMapping, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + bool bApplyPoseToRetargetBasePose, + class UIKRetargeterController* Controller + ) const; + USkeleton * AskUserForTargetSkeleton() const; + bool AskUserOverridingAssetsConfirmation(const TArray& AssetsToOverwrite) const; + /// Valid within a single method's stack space. + void GetAllSkeletalMeshesUsingSkeleton(const USkeleton * Skeleton, TArray & SkeletalMeshes) const; + void GetAllSkeletalMeshesUsingSkeleton(const USkeleton * Skeleton, TArray & SkeletalMeshes) const; + void SetPreviewMesh(USkeleton * Skeleton, USkeletalMesh * PreviewMesh) const; + + void EnumerateAssetsToOverwrite(const USkeleton* Skeleton, const USkeleton* ReferenceSkeleton, TArray& AssetsToOverride) const; + + UIKRigDefinition* CreateIKRig( + const FString & PackagePath, + const FString & AssetName, + const USkeleton* Skeleton + ) const; + UIKRigDefinition* CreateMixamoIKRig(const USkeleton* Skeleton) const; + UIKRigDefinition* CreateUEMannequinIKRig(const USkeleton* Skeleton, ETargetSkeletonType SkeletonType) const; + UIKRetargeter* CreateIKRetargeter( + const FString & PackagePath, + const FString & AssetName, + UIKRigDefinition* SourceRig, + UIKRigDefinition* TargetRig, + const FStaticNamesMapper & TargetToSource_ChainNamesMapping, + const TArray & TargetBoneChainsToSkip, + const TArray & TargetBoneChainsDriveIKGoal, + const TArray& TargetBoneChainsOneToOneRotationMode + ) const; + +private: + // UE4 Mannequin to Mixamo data. + const FStaticNamesMapper UE4MannequinToMixamo_BoneNamesMapping; + const FStaticNamesMapper UE4MannequinToMixamo_ChainNamesMapping; + // UE5/MetaHuman to Mixamo data. + const FStaticNamesMapper UE5MannequinToMixamo_BoneNamesMapping; + const FStaticNamesMapper UE5MannequinToMixamo_ChainNamesMapping; +}; diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.cpp new file mode 100644 index 00000000..1f99e201 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.cpp @@ -0,0 +1,33 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoToolkitCommands.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "MixamoToolkitStyle.h" + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +FMixamoToolkitCommands::FMixamoToolkitCommands() + : TCommands( + TEXT("MixamoAnimationRetargeting"), // Context name for fast lookup + NSLOCTEXT(LOCTEXT_NAMESPACE, "MixamoAnimationRetargetingCommands", "Mixamo Animation Retargeting Plugin"), + NAME_None, // Parent + FMixamoToolkitStyle::GetStyleSetName() // Icon Style Set + ) +{ +} + + + +void FMixamoToolkitCommands::RegisterCommands() +{ + //UI_COMMAND(OpenBatchConverterWindow, "Mixamo batch helper", "Open the batch helper for Mixamo assets", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(RetargetMixamoSkeleton, "Retarget Mixamo Skeleton Asset", "Retarget Mixamo Skeleton Assets", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ExtractRootMotion, "Generate Root Motion Animations", "Generate Root Motion Animations", EUserInterfaceActionType::Button, FInputChord()); +} + + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.h new file mode 100644 index 00000000..117b961f --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitCommands.h @@ -0,0 +1,21 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Framework/Commands/Commands.h" + + + +class FMixamoToolkitCommands : public TCommands +{ +public: + FMixamoToolkitCommands(); + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + //TSharedPtr< FUICommandInfo > OpenBatchConverterWindow; + TSharedPtr< FUICommandInfo > RetargetMixamoSkeleton; + TSharedPtr< FUICommandInfo > ExtractRootMotion; +}; \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.cpp new file mode 100644 index 00000000..09dc002b --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.cpp @@ -0,0 +1,315 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoToolkitEditorIntegration.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "MixamoAnimationRootMotionSolver.h" + +#include "MixamoToolkitCommands.h" +#include "MixamoToolkitStyle.h" + +#include "Animation/Skeleton.h" +#include "Animation/AnimSequence.h" + +//#include "LevelEditor.h" +#include "Editor/ContentBrowser/Public/ContentBrowserModule.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" + + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +void FMixamoToolkitEditorIntegration::Register() +{ + // Register RetargetMixamoSkeleton action into the Content Browser contextual menu. + // The contextual menu is built at run-time using the specified delegate. + { + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + + TArray & CBMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders(); + CBMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateSP(this, &FMixamoToolkitEditorIntegration::MakeContentBrowserContextMenuExtender)); + } +} + + + +void FMixamoToolkitEditorIntegration::Unregister() +{ +} + + + +FText FMixamoToolkitEditorIntegration::TooltipGetter_RetargetMixamoSkeletons() const +{ + TSharedRef< FUICommandInfo > cmd = FMixamoToolkitCommands::Get().RetargetMixamoSkeleton.ToSharedRef(); + + if (CanExecuteAction_RetargetMixamoSkeletons()) + { + return cmd->GetDescription(); + } + else + { + return FText::FromString(TEXT("WARNING: Retargeting is disabled because the selected asset is not recognized as a valid Mixamo skeleton.\n" + "Please read the documentation at https://www.unamedia.com/ue5-mixamo/docs/import-mixamo-character-in-ue5/#wrong_bones to solve the issue.")); + } +} + + + +FText FMixamoToolkitEditorIntegration::TooltipGetter_ExtractRootMotion() const +{ + TSharedRef< FUICommandInfo > cmd = FMixamoToolkitCommands::Get().ExtractRootMotion.ToSharedRef(); + + if (CanExecuteAction_ExtractRootMotion()) + { + return cmd->GetDescription(); + } + else + { + if (ContentBrowserSelectedAssets.Num() > 1) + { + return FText::FromString(TEXT("WARNING: Root motion extraction is disabled because you must select one skeleton at time.")); + } + else + { + return FText::FromString(TEXT("WARNING: Root motion extraction is disabled because the selected asset was not retargeted first.")); + } + } +} + + + +/* +### LEGAL NOTICE ### +### WARNING ### WARNING ### WARNING ### WARNING ### WARNING ### + +No changes to the code of this method are permitted, including any changes to other code that alter its intended execution. +Authors and organizations of any modifications and/or users and organizations using unauthorized modifications are considered to be in violation of the license terms. +For any need to customize this method you can contact UNAmedia. + +### WARNING ### WARNING ### WARNING ### WARNING ### WARNING ### +*/ +void FMixamoToolkitEditorIntegration::ExecuteChecked(TFunction AuthorizedCallback) const +{ + static const FText PluginFriendlyName = LOCTEXT("FMixamoToolkitEditorIntegration_PluginFriendlyName", "Mixamo Animation Retargeting 2"); + AuthorizedCallback(); +} + + + +/** Run the RetargetMixamoSkeleton action on the passed assets. */ +void FMixamoToolkitEditorIntegration::ExecuteAction_RetargetMixamoSkeletons() const +{ + // Get all USkeleton objects to process. + TArray Skeletons; + for (const FAssetData& Asset : ContentBrowserSelectedAssets) + { + if (CanExecuteAction_RetargetMixamoSkeleton(Asset)) + { + USkeleton * o = CastChecked (Asset.GetAsset()); + if (o != nullptr) + { + Skeletons.Add(o); + } + } + } + + const int32 NumOfWarningsBeforeRetargeting + = FMessageLog("LogMixamoToolkit").NumMessages(EMessageSeverity::Warning); + + FMixamoAnimationRetargetingModule::Get().GetMixamoSkeletonRetargeter()->RetargetToUE4Mannequin(Skeletons); + + // check if we have to open the message log window. + if (FMessageLog("LogMixamoToolkit").NumMessages(EMessageSeverity::Warning) != NumOfWarningsBeforeRetargeting) + { + FMessageLog("LogMixamoToolkit").Open(EMessageSeverity::Warning); + } +} + + + +/** Returns if the RetargetMixamoSkeleton action can run. */ +bool FMixamoToolkitEditorIntegration::CanExecuteAction_RetargetMixamoSkeleton(const FAssetData & Asset) const +{ + // Check the asset type. + if (Asset.AssetClassPath != USkeleton::StaticClass()->GetClassPathName()) + { + return false; + } + // Check the asset content. + // NOTE: this will load the asset if needed. + if (!FMixamoAnimationRetargetingModule::Get().GetMixamoSkeletonRetargeter()->IsMixamoSkeleton(Cast (Asset.GetAsset()))) + { + return false; + } + + return true; +} + + + +/** Returns if the RetargetMixamoSkeleton action can run on selected assets (editor will gray-out it otherwise). */ +bool FMixamoToolkitEditorIntegration::CanExecuteAction_RetargetMixamoSkeletons() const +{ + // Return true if any of SelectedAssets can be processed. + return ContentBrowserSelectedAssets.ContainsByPredicate( + [this](const FAssetData & asset) + { + return CanExecuteAction_RetargetMixamoSkeleton(asset); + } + ); +} + + + +void FMixamoToolkitEditorIntegration::ExecuteAction_ExtractRootMotion() const +{ + // Get all USkeleton objects to process. + USkeleton * Skeleton(nullptr); + for (const FAssetData& Asset : ContentBrowserSelectedAssets) + { + if (CanExecuteAction_ExtractRootMotion(Asset)) + { + USkeleton * o = CastChecked(Asset.GetAsset()); + if (o != nullptr) + { + Skeleton = o; + break; + } + } + } + + const int32 NumOfWarningsBeforeRetargeting + = FMessageLog("LogMixamoToolkit").NumMessages(EMessageSeverity::Warning); + + FMixamoAnimationRetargetingModule::Get().GetMixamoAnimationRootMotionSolver()->LaunchProcedureFlow(Skeleton); + + // check if we have to open the message log window. + if (FMessageLog("LogMixamoToolkit").NumMessages(EMessageSeverity::Warning) != NumOfWarningsBeforeRetargeting) + { + FMessageLog("LogMixamoToolkit").Open(EMessageSeverity::Warning); + } +} + + + +bool FMixamoToolkitEditorIntegration::CanExecuteAction_ExtractRootMotion(const FAssetData & Asset) const +{ + // Check the asset type. + if (Asset.AssetClassPath != USkeleton::StaticClass()->GetClassPathName()) + { + return false; + } + + USkeleton* Skeleton = Cast(Asset.GetAsset()); + return FMixamoAnimationRetargetingModule::Get().GetMixamoAnimationRootMotionSolver()->CanExecuteProcedure(Skeleton); +} + + + +bool FMixamoToolkitEditorIntegration::CanExecuteAction_ExtractRootMotion() const +{ + if (ContentBrowserSelectedAssets.Num() != 1) + return false; + + // Return true if any of SelectedAssets can be processed. + return ContentBrowserSelectedAssets.ContainsByPredicate( + [this](const FAssetData & asset) + { + return CanExecuteAction_ExtractRootMotion(asset); + } + ); +} + + + +/** Called when the ContentBrowser asks for extenders on selected assets. */ +TSharedRef FMixamoToolkitEditorIntegration::MakeContentBrowserContextMenuExtender(const TArray & NewSelectedAssets) +{ + ContentBrowserSelectedAssets = NewSelectedAssets; + + TSharedRef Extender(new FExtender()); + + // Enable the action on supported asset types, use CanExecuteAction_RetargetMixamoSkeleton() to check later + // if the asset object can be affected. + bool bAnySupportedAssets = false; + for (const FAssetData& Asset: ContentBrowserSelectedAssets) + { + bAnySupportedAssets = bAnySupportedAssets || (Asset.AssetClassPath == USkeleton::StaticClass()->GetClassPathName()); + } + + if (bAnySupportedAssets) + { + // Add the actions to the extender + Extender->AddMenuExtension( + "GetAssetActions", + EExtensionHook::After, + PluginCommands, + // To use an intermediary sub-menu: FMenuExtensionDelegate::CreateSP(this, &FMixamoToolkitEditorIntegration::AddContentBrowserContextSubMenu) + FMenuExtensionDelegate::CreateSP(this, &FMixamoToolkitEditorIntegration::AddContentBrowserContextMenuEntries) + ); + } + + return Extender; +} + + + +void FMixamoToolkitEditorIntegration::AddContentBrowserContextSubMenu(class FMenuBuilder& MenuBuilder) const +{ + // Add the submenu only if we can execute some actions. + if (!CanExecuteAction_RetargetMixamoSkeletons()) + { + return; + } + + MenuBuilder.AddSubMenu( + LOCTEXT("FMixamoToolkitEditorIntegration_ContentBrowser_SubMenuLabel", "Mixamo Asset Actions"), + LOCTEXT("FMixamoToolkitEditorIntegration_ContentBrowser_SubMenuToolTip", "Other Mixamo Asset Actions"), + FNewMenuDelegate::CreateSP(this, &FMixamoToolkitEditorIntegration::AddContentBrowserContextMenuEntries), + FUIAction(), + NAME_None, // InExtensionHook + EUserInterfaceActionType::Button, + false, // bInOpenSubMenuOnClick + FSlateIcon(FMixamoToolkitStyle::GetStyleSetName(), "ContentBrowser.AssetActions") + ); +} + + + +void FMixamoToolkitEditorIntegration::AddContentBrowserContextMenuEntries(class FMenuBuilder& MenuBuilder) const +{ + TFunction RetargetMixamoSkeletonsFunction = [this]() { ExecuteAction_RetargetMixamoSkeletons(); }; + + // Add the RetargetMixamoSkeleton action. + TSharedRef< FUICommandInfo > cmd = FMixamoToolkitCommands::Get().RetargetMixamoSkeleton.ToSharedRef(); + MenuBuilder.AddMenuEntry( + cmd->GetLabel(), + TAttribute::CreateSP(this, &FMixamoToolkitEditorIntegration::TooltipGetter_RetargetMixamoSkeletons), + cmd->GetIcon(), + FUIAction( + FExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::ExecuteChecked, RetargetMixamoSkeletonsFunction), + FCanExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::CanExecuteAction_RetargetMixamoSkeletons) + ) + ); + + TFunction ExtractRootMotionFunction = [this]() { ExecuteAction_ExtractRootMotion(); }; + + // Add the ExtractRootMotion action. + cmd = FMixamoToolkitCommands::Get().ExtractRootMotion.ToSharedRef(); + MenuBuilder.AddMenuEntry( + cmd->GetLabel(), + TAttribute::CreateSP(this, &FMixamoToolkitEditorIntegration::TooltipGetter_ExtractRootMotion), + cmd->GetIcon(), + FUIAction( + FExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::ExecuteChecked, ExtractRootMotionFunction), + FCanExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::CanExecuteAction_ExtractRootMotion) + ) + ); +} + + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.h new file mode 100644 index 00000000..2e913184 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitEditorIntegration.h @@ -0,0 +1,43 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include + + + +class FMixamoToolkitEditorIntegration : public TSharedFromThis +{ +public: + void Register(); + void Unregister(); + +private: + TSharedPtr PluginCommands; + // Store currently selected assets from Content Browser here to avoid passing them in lambda closures. + TArray ContentBrowserSelectedAssets; + +private: + // Tooltips. + FText TooltipGetter_RetargetMixamoSkeletons() const; + FText TooltipGetter_ExtractRootMotion() const; + +private: + + void ExecuteChecked(TFunction AuthorizedCallback) const; + +private: + + // Actions. + void ExecuteAction_RetargetMixamoSkeletons() const; + bool CanExecuteAction_RetargetMixamoSkeleton(const FAssetData & Asset) const; + bool CanExecuteAction_RetargetMixamoSkeletons() const; + + void ExecuteAction_ExtractRootMotion() const; + bool CanExecuteAction_ExtractRootMotion(const FAssetData & Asset) const; + bool CanExecuteAction_ExtractRootMotion() const; + + TSharedRef MakeContentBrowserContextMenuExtender(const TArray & NewSelectedAssets); + void AddContentBrowserContextSubMenu(class FMenuBuilder& MenuBuilder) const; + void AddContentBrowserContextMenuEntries(class FMenuBuilder& MenuBuilder) const; +}; \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivate.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivate.h new file mode 100644 index 00000000..d61b15d0 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivate.h @@ -0,0 +1,31 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Modules/ModuleManager.h" + + +DECLARE_LOG_CATEGORY_EXTERN(LogMixamoToolkit, Warning, All) + + + +class FMixamoAnimationRetargetingModule : + public IModuleInterface, + public TSharedFromThis +{ +public: + static + FMixamoAnimationRetargetingModule & Get(); + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + TSharedRef GetMixamoSkeletonRetargeter(); + TSharedRef GetMixamoAnimationRootMotionSolver(); + +private: + TSharedPtr MixamoSkeletonRetargeter; + TSharedPtr MixamoAnimationRootMotionSolver; + TSharedPtr EditorIntegration; +}; \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivatePCH.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivatePCH.h new file mode 100644 index 00000000..3f944e2a --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitPrivatePCH.h @@ -0,0 +1,22 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "Runtime/Launch/Resources/Version.h" + +#include "MixamoAnimationRetargeting.h" + +// You should place include statements to your module's private header files here. You only need to +// add includes for headers that are used in most of your module's source files though. + +#include "MixamoToolkitPrivate.h" + + + +// Forward declaration of FAssetData +#if (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION >= 17) || (ENGINE_MAJOR_VERSION > 4) +// https://github.com/EpicGames/UnrealEngine/commit/70d3bd4b726884ccd6fe348fa45f11252aa99e04 +// Change 3439819 by Matt.Kuhlenschmidt +// Turned FAssetData into a struct for some upcoming script exposure of FAssetData +struct FAssetData; +#else +class FAssetData; +#endif diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.cpp new file mode 100644 index 00000000..35804719 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.cpp @@ -0,0 +1,95 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "MixamoToolkitStyle.h" +#include "MixamoToolkitPrivatePCH.h" +#include "Slate/SlateGameResources.h" +#include "Styling/SlateStyleRegistry.h" +#include "Interfaces/IPluginManager.h" +#include "Framework/Application/SlateApplication.h" + + +#define PLUGIN_NAME "MixamoAnimationRetargeting" + + + +TSharedPtr< FSlateStyleSet > FMixamoToolkitStyle::StyleInstance = NULL; + + + +void FMixamoToolkitStyle::Initialize() +{ + if (! StyleInstance.IsValid()) + { + StyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*StyleInstance); + } +} + + + +void FMixamoToolkitStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*StyleInstance); + ensure(StyleInstance.IsUnique()); + StyleInstance.Reset(); +} + + + +FName FMixamoToolkitStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT(PLUGIN_NAME "Style")); + return StyleSetName; +} + + + +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) +#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + + +TSharedRef< FSlateStyleSet > FMixamoToolkitStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet(PLUGIN_NAME "Style")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin(PLUGIN_NAME)->GetBaseDir() / TEXT("Resources")); + + // Define the styles for the module's actions. + // For commands: the command name/id must match the style's property name. + Style->Set(PLUGIN_NAME ".RetargetMixamoSkeleton", new IMAGE_BRUSH(TEXT("ButtonIcon_40x"), Icon40x40)); + Style->Set(PLUGIN_NAME ".ExtractRootMotion", new IMAGE_BRUSH(TEXT("ButtonIcon_40x"), Icon40x40)); + + Style->Set("ContentBrowser.AssetActions", new IMAGE_BRUSH(TEXT("ButtonIcon_40x"), Icon40x40)); + + return Style; +} + + +#undef IMAGE_BRUSH +#undef BOX_BRUSH +#undef BORDER_BRUSH +#undef TTF_FONT +#undef OTF_FONT + + + +void FMixamoToolkitStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + + + +const ISlateStyle& FMixamoToolkitStyle::Get() +{ + return *StyleInstance; +} diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.h new file mode 100644 index 00000000..bfbf7f07 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/MixamoToolkitStyle.h @@ -0,0 +1,31 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + + + +class ISlateStyle; + + + +/** Slate styles used by this plugin. */ +class FMixamoToolkitStyle +{ +public: + static void Initialize(); + static void Shutdown(); + + /** reloads textures used by slate renderer */ + static void ReloadTextures(); + + /** @return The Slate style set for the Shooter game */ + static const ISlateStyle& Get(); + + static FName GetStyleSetName(); + +private: + static TSharedRef< class FSlateStyleSet > Create(); + +private: + static TSharedPtr< class FSlateStyleSet > StyleInstance; +}; \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.cpp new file mode 100644 index 00000000..35606150 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.cpp @@ -0,0 +1,104 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "NamesMapper.h" + + + +namespace +{ + +/** +Iterate over a "column" of data of a "mapping table". + +@param Table The mapping table. It's assumed to be a continuos array of "items", where each item is a pair of "elements" stored continuously. +@param TableNum The number of items in the Table (this means that the Table contains TableNum*2 elements). +@param iColumn The column index of the elements we want to iterate over. +@param Functor The callback called for each iterated element. Prototype is void(const TTableItem & Elem). + +If needed, we can get rid of the template using TFunctionRef<>. +*/ +template +void IterateOnMappingTable(const TTableItem * Table, int32 TableNum, int32 iColumn, TFunctor Functor) +{ + for (int32 i = 0; i < TableNum; i += 2) + { + Functor(Table[i + iColumn]); + } +} + +template +void GetMappingTableColumn(const TTableItem * Table, int32 TableNum, int32 iColumn, TContainer & Output) +{ + Output.Reset(TableNum / 2); + IterateOnMappingTable(Table, TableNum, iColumn, [&](const TTableItem & Item) { Output.Emplace(Item); }); +} + +} // namespace *unnamed* + + + +FStaticNamesMapper::FStaticNamesMapper( + const char* const* SourceToDestinationMapping, + int32 SourceToDestinationMappingNum, + bool Reverse +) + : Mapping(SourceToDestinationMapping), + MappingNum(SourceToDestinationMappingNum), + SrcOfs(Reverse ? 1 : 0), + DstOfs(Reverse ? 0 : 1) +{ + check(Mapping != nullptr); + checkf(MappingNum % 2 == 0, TEXT("The input array is expeted to have an even number of entries")); +} + + + +FName FStaticNamesMapper::MapName(const FName& SourceName) const +{ + for (int32 i = 0; i < MappingNum; i += 2) + { + if (FName(Mapping[i + SrcOfs]) == SourceName) + { + return FName(Mapping[i + DstOfs]); + } + } + return NAME_None; +} + + + +TArray FStaticNamesMapper::MapNames(const TArray& Names) const +{ + TArray Result; + Result.Reserve(Names.Num()); + for (const FName & Name : Names) + { + const FName MappedName = MapName(Name); + if (!MappedName.IsNone()) + { + Result.Emplace(MappedName); + } + } + return Result; +} + + + +FStaticNamesMapper FStaticNamesMapper::GetInverseMapper() const +{ + return FStaticNamesMapper(Mapping, MappingNum, SrcOfs == 0); +} + + + +void FStaticNamesMapper::GetSource(TArray& Out) const +{ + GetMappingTableColumn(Mapping, MappingNum, 0, Out); +} + + + +void FStaticNamesMapper::GetDestination(TArray& Out) const +{ + GetMappingTableColumn(Mapping, MappingNum, 1, Out); +} diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.h new file mode 100644 index 00000000..866aca82 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/NamesMapper.h @@ -0,0 +1,38 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Engine/EngineTypes.h" + + + +/** +Class to map FName objects, using an aliased mapping table. + +@attention The mapping table is aliased and it's lifecycle must include the one of this object. + Usually you should initialize it with a static/constexpr table. +*/ +class FStaticNamesMapper +{ +public: + FStaticNamesMapper(const char* const* SourceToDestinationMapping, int32 SourceToDestinationMappingNum, bool Reverse = false); + + /** + Map a bone index from the source skeleton to a bone index into the destination skeleton. + + Returns INDEX_NONE if the bone can't be mapped. + */ + FName MapName(const FName & SourceName) const; + /// Map a set of names. + TArray MapNames(const TArray& Names) const; + FStaticNamesMapper GetInverseMapper() const; + + void GetSource(TArray& Out) const; + void GetDestination(TArray& Out) const; + +protected: + const char* const* Mapping; + int32 MappingNum; + int32 SrcOfs; + int32 DstOfs; +}; \ No newline at end of file diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.cpp new file mode 100644 index 00000000..611ce400 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.cpp @@ -0,0 +1,618 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "SMixamoToolkitWidget.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "Widgets/SBoxPanel.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Layout/SUniformGridPanel.h" + +#include "SAssetView.h" + +#include "Features/IModularFeatures.h" +#include "IContentBrowserDataModule.h" +#include "ContentBrowserModule.h" +#include "ContentBrowserDataSource.h" +#include "ContentBrowserAssetDataSource.h" +#include "ContentBrowserAssetDataCore.h" +#include "IContentBrowserSingleton.h" // FAssetPickerConfig +#include "Animation/AnimSequence.h" +#include "Animation/Skeleton.h" +#include "Animation/Rig.h" + + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +SRiggedSkeletonPicker::SRiggedSkeletonPicker() + : ActiveSkeleton(nullptr), + SelectedSkeleton(nullptr) +{} + + + +void SRiggedSkeletonPicker::Construct(const FArguments& InArgs) +{ + checkf(!InArgs._Title.IsEmpty(), TEXT("A title must be specified.")); + checkf(!InArgs._Description.IsEmpty(), TEXT("A description must be specified.")); + + ActiveSkeleton = nullptr; + SelectedSkeleton = nullptr; + + // Configure the Asset Picker. + FAssetPickerConfig AssetPickerConfig; + AssetPickerConfig.Filter.ClassPaths.Add(USkeleton::StaticClass()->GetClassPathName()); + AssetPickerConfig.Filter.bRecursiveClasses = true; + AssetPickerConfig.SelectionMode = ESelectionMode::Single; + AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SRiggedSkeletonPicker::OnAssetSelected); + AssetPickerConfig.AssetShowWarningText = LOCTEXT("SRiggedSkeletonPicker_NoAssets", "No Skeleton asset for the UE Mannequin found!"); + AssetPickerConfig.OnShouldFilterAsset = InArgs._OnShouldFilterAsset; + // Aesthetic settings. + AssetPickerConfig.OnAssetDoubleClicked = FOnAssetDoubleClicked::CreateSP(this, &SRiggedSkeletonPicker::OnAssetDoubleClicked); + AssetPickerConfig.InitialAssetViewType = EAssetViewType::Column; + AssetPickerConfig.bShowPathInColumnView = true; + AssetPickerConfig.bShowTypeInColumnView = false; + // Hide all asset registry columns by default (we only really want the name and path) + TArray AssetRegistryTags; + USkeleton::StaticClass()->GetDefaultObject()->GetAssetRegistryTags(AssetRegistryTags); + for (UObject::FAssetRegistryTag & AssetRegistryTag : AssetRegistryTags) + { + AssetPickerConfig.HiddenColumnNames.Add(AssetRegistryTag.Name.ToString()); + } + + FContentBrowserModule & ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + TSharedRef AssetPicker = ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig); + + ChildSlot[ + SNew(SVerticalBox) + + // Title text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + .HAlign(HAlign_Fill) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(InArgs._Title) + .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) + .AutoWrapText(true) + ] + ] + + // Help description text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + .HAlign(HAlign_Fill) + [ + SNew(STextBlock) + .Text(InArgs._Description) + .AutoWrapText(true) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Asset picker. + + SVerticalBox::Slot() + .MaxHeight(500) + [ + AssetPicker + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Buttons + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Bottom) + [ + SNew(SUniformGridPanel) + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRiggedSkeletonPicker_Ok", "Select")) + .IsEnabled(this, &SRiggedSkeletonPicker::CanSelect) + .OnClicked(this, &SRiggedSkeletonPicker::OnSelect) + ] + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRiggedSkeletonPicker_Cancel", "Cancel")) + .OnClicked(this, &SRiggedSkeletonPicker::OnCancel) + ] + ] + ]; +} + + + +USkeleton * SRiggedSkeletonPicker::GetSelectedSkeleton() +{ + return SelectedSkeleton; +} + + + +void SRiggedSkeletonPicker::OnAssetSelected(const FAssetData & AssetData) +{ + ActiveSkeleton = Cast(AssetData.GetAsset()); +} + + + +void SRiggedSkeletonPicker::OnAssetDoubleClicked(const FAssetData & AssetData) +{ + OnAssetSelected(AssetData); + OnSelect(); +} + + + +bool SRiggedSkeletonPicker::CanSelect() const +{ + return ActiveSkeleton != nullptr; +} + + + +FReply SRiggedSkeletonPicker::OnSelect() +{ + SelectedSkeleton = ActiveSkeleton; + CloseWindow(); + return FReply::Handled(); +} + + + +FReply SRiggedSkeletonPicker::OnCancel() +{ + SelectedSkeleton = nullptr; + CloseWindow(); + return FReply::Handled(); +} + + + +void SRiggedSkeletonPicker::CloseWindow() +{ + TSharedPtr window = FSlateApplication::Get().FindWidgetWindow(AsShared()); + if (window.IsValid()) + { + window->RequestDestroyWindow(); + } +} + + + +SRootMotionExtractionWidget::SRootMotionExtractionWidget() + : ActiveAnimationSequence(nullptr), + ActiveInPlaceAnimationSequence(nullptr), + SelectedAnimationSequence(nullptr), + SelectedInPlaceAnimationSequence(nullptr) +{} + + + +void SRootMotionExtractionWidget::Construct(const FArguments& InArgs) +{ + const USkeleton * ReferenceSkeleton = InArgs._ReferenceSkeleton; + + checkf(ReferenceSkeleton != nullptr, TEXT("A reference skeleton must be specified.")); + + FText Title = LOCTEXT("SRootMotionExtractionWidget_Title", "Generate Root Motion Animation"); + FText Description = LOCTEXT("SRootMotionExtractionWidget_Description", "You can generate a Root Motion animation from an ordinary Mixamo animation and its in-place version. A new asset will be created."); + FText NormalAnimPickerDesc = LOCTEXT("SRootMotionExtractionWidget_NormalAnimPickerDescription", "ORDINARY animation."); + FText InPlaceAnimPickerDesc = LOCTEXT("SRootMotionExtractionWidget_InPlaceAnimPickerDescription", "IN-PLACE animation."); + + ActiveAnimationSequence = nullptr; + ActiveInPlaceAnimationSequence = nullptr; + SelectedAnimationSequence = nullptr; + SelectedInPlaceAnimationSequence = nullptr; + + ChildSlot[ + SNew(SVerticalBox) + + // Title text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + [ + SNew(STextBlock) + .Text(Title) + .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) + .AutoWrapText(true) + ] + + // Help description text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + [ + SNew(STextBlock) + .Text(Description) + .AutoWrapText(true) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Asset pickers. + + SVerticalBox::Slot() + .FillHeight(1) + .Padding(2) + .MaxHeight(500) + [ + SNew(SHorizontalBox) + + // Picker for "normal" animation + + SHorizontalBox::Slot() + .FillWidth(1) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Center) + .Padding(5) + [ + SNew(STextBlock) + .Text(NormalAnimPickerDesc) + .AutoWrapText(true) + ] + + + SVerticalBox::Slot() + .FillHeight(1) + [ + CreateAnimationSequencePicker(ReferenceSkeleton, false) + ] + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Picker for "in-place" animation + + SHorizontalBox::Slot() + .FillWidth(1) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Center) + .Padding(5) + [ + SNew(STextBlock) + .Text(InPlaceAnimPickerDesc) + .AutoWrapText(true) + ] + + + SVerticalBox::Slot() + .FillHeight(1) + [ + CreateAnimationSequencePicker(ReferenceSkeleton, true) + ] + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Buttons + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Bottom) + [ + SNew(SUniformGridPanel) + + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRootMotionExtractionWidget_Ok", "Select")) + .IsEnabled(this, &SRootMotionExtractionWidget::CanSelect) + .OnClicked(this, &SRootMotionExtractionWidget::OnSelect) + ] + + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRootMotionExtractionWidget_Cancel", "Cancel")) + .OnClicked(this, &SRootMotionExtractionWidget::OnCancel) + ] + ] + ]; +} + + + +TSharedRef SRootMotionExtractionWidget::CreateAnimationSequencePicker(const USkeleton * ReferenceSkeleton, bool InPlaceAnimation) +{ + auto OnClickDelegate = [this, InPlaceAnimation](const FAssetData & AssetData) + { + UAnimSequence* anim = Cast(AssetData.GetAsset()); + if (InPlaceAnimation) + ActiveInPlaceAnimationSequence = anim; + else + ActiveAnimationSequence = anim; + }; + + // Configure the Asset Picker. + FAssetPickerConfig AssetPickerConfig; + AssetPickerConfig.Filter.ClassPaths.Add(UAnimSequence::StaticClass()->GetClassPathName()); + AssetPickerConfig.Filter.bRecursiveClasses = true; + if (ReferenceSkeleton != nullptr) + { + FString SkeletonString = FAssetData(ReferenceSkeleton).GetExportTextName(); + AssetPickerConfig.Filter.TagsAndValues.Add(FName(TEXT("Skeleton")), SkeletonString); + } + AssetPickerConfig.SelectionMode = ESelectionMode::Single; + AssetPickerConfig.OnAssetSelected.BindLambda(OnClickDelegate); + AssetPickerConfig.AssetShowWarningText = LOCTEXT("SRootMotionExtractionWidget_NoAnimations", "No Animation asset for the selected Skeleton found!"); + // Aesthetic settings. + AssetPickerConfig.OnAssetDoubleClicked.BindLambda(OnClickDelegate); + AssetPickerConfig.InitialAssetViewType = EAssetViewType::Column; + AssetPickerConfig.bShowPathInColumnView = true; + AssetPickerConfig.bShowTypeInColumnView = false; + // Hide all asset registry columns by default (we only really want the name and path) + TArray AssetRegistryTags; + UAnimSequence::StaticClass()->GetDefaultObject()->GetAssetRegistryTags(AssetRegistryTags); + for (UObject::FAssetRegistryTag & AssetRegistryTag : AssetRegistryTags) + { + AssetPickerConfig.HiddenColumnNames.Add(AssetRegistryTag.Name.ToString()); + } + + FContentBrowserModule & ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + return ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig); +} + + + +bool SRootMotionExtractionWidget::CanSelect() const +{ + return ActiveAnimationSequence != nullptr + && ActiveInPlaceAnimationSequence != nullptr + && ActiveAnimationSequence != ActiveInPlaceAnimationSequence; +} + + + +FReply SRootMotionExtractionWidget::OnSelect() +{ + SelectedAnimationSequence = ActiveAnimationSequence; + SelectedInPlaceAnimationSequence = ActiveInPlaceAnimationSequence; + CloseWindow(); + return FReply::Handled(); +} + + + +FReply SRootMotionExtractionWidget::OnCancel() +{ + SelectedAnimationSequence = nullptr; + SelectedInPlaceAnimationSequence = nullptr; + CloseWindow(); + return FReply::Handled(); +} + + + +void SRootMotionExtractionWidget::CloseWindow() +{ + TSharedPtr window = FSlateApplication::Get().FindWidgetWindow(AsShared()); + if (window.IsValid()) + { + window->RequestDestroyWindow(); + } +} + +SOverridingAssetsConfirmationDialog::SOverridingAssetsConfirmationDialog() + : bConfirmed(false) +{ +} + +FReply SOverridingAssetsConfirmationDialog::OnConfirm() +{ + bConfirmed = true; + CloseWindow(); + return FReply::Handled(); +} + +FReply SOverridingAssetsConfirmationDialog::OnCancel() +{ + bConfirmed = false; + CloseWindow(); + return FReply::Handled(); +} + +void SOverridingAssetsConfirmationDialog::CloseWindow() +{ + TSharedPtr window = FSlateApplication::Get().FindWidgetWindow(AsShared()); + if (window.IsValid()) + { + window->RequestDestroyWindow(); + } +} + +bool SOverridingAssetsConfirmationDialog::EnumerateCustomSourceItemDatas(TFunctionRef InCallback) +{ + UContentBrowserDataSubsystem* ContentBrowserDataSubsystem = IContentBrowserDataModule::Get().GetSubsystem(); + +#if 0 + TArray SourceItemPaths; + for (auto Asset : AssetsToOverwrite) + { + SourceItemPaths.Add(FContentBrowserItemPath(FAssetData(Asset).PackageName, EContentBrowserPathType::Internal)); + } + + //BUG: assets in memory are not enumerated. + return ContentBrowserDataSubsystem->EnumerateItemsAtPaths(SourceItemPaths, EContentBrowserItemTypeFilter::IncludeFiles, InCallback); +#else + + IModularFeatures& ModularFeatures = IModularFeatures::Get(); + static const FName BrowserDataSourceTypeName = UContentBrowserDataSource::GetModularFeatureTypeName(); + + const int32 NumExtensions = ModularFeatures.GetModularFeatureImplementationCount(BrowserDataSourceTypeName); + for (int32 ExtensionIndex = 0; ExtensionIndex < NumExtensions; ++ExtensionIndex) + { + UContentBrowserDataSource* DataSource = static_cast(ModularFeatures.GetModularFeatureImplementation(BrowserDataSourceTypeName, ExtensionIndex)); + if (DataSource->IsA()) + { + for (auto Asset : AssetsToOverwrite) + { + FAssetData AssetData(Asset); + + FName VirtualizedPath; + DataSource->TryConvertInternalPathToVirtual(FName(AssetData.GetObjectPathString()), VirtualizedPath); + + InCallback(ContentBrowserAssetData::CreateAssetFileItem(DataSource, VirtualizedPath, AssetData)); + } + + break; + } + } + + return true; +#endif +} + +void SOverridingAssetsConfirmationDialog::Construct(const FArguments& InArgs) +{ + FText Title = LOCTEXT("SOverridingAssetsConfirmationDialog_Title", "Warning"); + FText Description = LOCTEXT("SOverridingAssetsConfirmationDialog_Description", "Files listed below will be overwritten! Please confirm to continue or cancel to abort the procedure."); + + AssetsToOverwrite = InArgs._AssetsToOverwrite; + + auto LibrarySourceData = MakeShared(); + // Provide a dummy invalid virtual path to make sure nothing tries to enumerate root "/" + LibrarySourceData->VirtualPaths.Add(FName(TEXT("/UMGWidgetTemplateListViewModel"))); + // Disable any enumerate of virtual path folders + LibrarySourceData->bIncludeVirtualPaths = false; + // Supply a custom list of source items to display + LibrarySourceData->OnEnumerateCustomSourceItemDatas.BindSP(this, &SOverridingAssetsConfirmationDialog::EnumerateCustomSourceItemDatas); + + TSharedRef AssetView = SNew(SAssetView) + .InitialCategoryFilter(EContentBrowserItemCategoryFilter::IncludeAll) + .InitialSourcesData(*LibrarySourceData) + .InitialViewType(EAssetViewType::List) + //.InitialThumbnailPoolSize(AssetsToOverwrite.Num()) + //.InitialThumbnailSize(EThumbnailSize::Large) + .ForceShowEngineContent(true) + .ForceShowPluginContent(true) + .ShowTypeInTileView(false) + .ShowViewOptions(false); + + ChildSlot[ + SNew(SVerticalBox) + + // Title text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + .HAlign(HAlign_Fill) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(Title) + .Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 16)) + .AutoWrapText(true) + ] + ] + + // Help description text + + SVerticalBox::Slot() + .AutoHeight() + .Padding(2) + .HAlign(HAlign_Fill) + [ + SNew(STextBlock) + .Text(Description) + .AutoWrapText(true) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Asset viewer. + + SVerticalBox::Slot() + .MaxHeight(500) + [ + AssetView + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SSeparator) + ] + + // Buttons + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Bottom) + [ + SNew(SUniformGridPanel) + + SUniformGridPanel::Slot(0, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRiggedSkeletonPicker_Ok", "Confirm")) + .OnClicked(this, &SOverridingAssetsConfirmationDialog::OnConfirm) + ] + + SUniformGridPanel::Slot(1, 0) + [ + SNew(SButton) + .HAlign(HAlign_Center) + .Text(LOCTEXT("SRiggedSkeletonPicker_Cancel", "Cancel")) + .OnClicked(this, &SOverridingAssetsConfirmationDialog::OnCancel) + ] + ] + ]; + + AssetView->RequestSlowFullListRefresh(); +} + + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.h new file mode 100644 index 00000000..e016b4e1 --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SMixamoToolkitWidget.h @@ -0,0 +1,114 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Widgets/SCompoundWidget.h" + +class FContentBrowserItemData; + +class SRiggedSkeletonPicker : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_RetVal_OneParam(bool, FOnShouldFilterAsset, const FAssetData& /*AssetData*/); + + SLATE_BEGIN_ARGS(SRiggedSkeletonPicker) + {} + + SLATE_ARGUMENT(FText, Title) + SLATE_ARGUMENT(FText, Description) + /** Called to check if an asset is valid to use */ + SLATE_EVENT(FOnShouldFilterAsset, OnShouldFilterAsset) + SLATE_END_ARGS() + +public: + SRiggedSkeletonPicker(); + void Construct(const FArguments& InArgs); + + USkeleton * GetSelectedSkeleton(); + +private: + void OnAssetSelected(const FAssetData & AssetData); + void OnAssetDoubleClicked(const FAssetData & AssetData); + bool CanSelect() const; + FReply OnSelect(); + FReply OnCancel(); + + void CloseWindow(); + +private: + // Track in ActiveSkeleton the temporary selected asset, + // only after the user confirms set SelectedSkeleton. So if + // the widget is externally closed we don't report an un-selected + // asset. + USkeleton * ActiveSkeleton; + USkeleton * SelectedSkeleton; +}; + + + +class SRootMotionExtractionWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SRootMotionExtractionWidget) + : _ReferenceSkeleton(nullptr) + {} + + SLATE_ARGUMENT(USkeleton*, ReferenceSkeleton) + SLATE_END_ARGS() + +public: + SRootMotionExtractionWidget(); + void Construct(const FArguments& InArgs); + + UAnimSequence * GetSelectedAnimation() { return SelectedAnimationSequence; } + UAnimSequence * GetSelectedInPlaceAnimation() { return SelectedInPlaceAnimationSequence; } + +private: + bool CanSelect() const; + FReply OnSelect(); + FReply OnCancel(); + + void CloseWindow(); + + TSharedRef CreateAnimationSequencePicker(const USkeleton * ReferenceSkeleton, bool InPlaceAnimation); + +private: + // Track in ActiveXYZ the temporary selected assets, only after the user confirms + // set SelectedXYZ properties. + // So if the widget is externally closed we don't report errors for un-selected assets. + UAnimSequence * ActiveAnimationSequence; + UAnimSequence * ActiveInPlaceAnimationSequence; + + UAnimSequence * SelectedAnimationSequence; + UAnimSequence * SelectedInPlaceAnimationSequence; +}; + +class SOverridingAssetsConfirmationDialog : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SOverridingAssetsConfirmationDialog) + {} + SLATE_ARGUMENT(TArray, AssetsToOverwrite) + SLATE_END_ARGS() + +public: + SOverridingAssetsConfirmationDialog(); + void Construct(const FArguments& InArgs); + + bool HasConfirmed() const { return bConfirmed; } + +private: + + FReply OnConfirm(); + FReply OnCancel(); + + void CloseWindow(); + + bool EnumerateCustomSourceItemDatas(TFunctionRef InCallback); + +private: + + TArray AssetsToOverwrite; + bool bConfirmed; +}; diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.cpp new file mode 100644 index 00000000..b7f5d53d --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.cpp @@ -0,0 +1,49 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "SkeletonMatcher.h" + +#include + + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +FSkeletonMatcher::FSkeletonMatcher( + const TArray& InBoneNames, + float InMinimumMatchingPerc +) + : BoneNames(InBoneNames), + MinimumMatchingPerc(InMinimumMatchingPerc) +{} + + + +bool FSkeletonMatcher::IsMatching(const USkeleton* Skeleton) const +{ + // No Skeleton, No matching... + if (Skeleton == nullptr) + { + return false; + } + + const int32 NumExpectedBones = BoneNames.Num(); + int32 nMatchingBones = 0; + const FReferenceSkeleton & SkeletonRefSkeleton = Skeleton->GetReferenceSkeleton(); + for (int32 i = 0; i < NumExpectedBones; ++i) + { + const int32 BoneIndex = SkeletonRefSkeleton.FindBoneIndex(BoneNames[i]); + if (BoneIndex != INDEX_NONE) + { + ++nMatchingBones; + } + } + const float MatchedPercentage = float(nMatchingBones) / float(NumExpectedBones); + + return MatchedPercentage >= MinimumMatchingPerc; +} + + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.h new file mode 100644 index 00000000..c908244c --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonMatcher.h @@ -0,0 +1,32 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Engine/EngineTypes.h" + + + +class USkeleton; + + + +/** +Class to check if a skeleton is matching a desired hierarchy. + +@attention To be used within a single method's stack space. +*/ +class FSkeletonMatcher +{ +public: + /** + @param InBoneNames The expected bone names. + @param InMinimumMatchingPerc A skeleton is matching if it has at least X% of the expected bones. The value is in [0, 1]. + */ + FSkeletonMatcher(const TArray & InBoneNames, float InMinimumMatchingPerc); + + bool IsMatching(const USkeleton * Skeleton) const; + +private: + const TArray BoneNames; + const float MinimumMatchingPerc; +}; diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.cpp b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.cpp new file mode 100644 index 00000000..72a4eebd --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.cpp @@ -0,0 +1,509 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#include "SkeletonPoser.h" +#include "MixamoToolkitPrivatePCH.h" + +#include "Engine/SkeletalMesh.h" +#include "Animation/Skeleton.h" +#include "Animation/Rig.h" + +#include "Retargeter/IKRetargeter.h" + +#include + + + +// Define it to check mathematical equivalences used by the algorithm. +// NOTE: leave it undefined on redistribution, as ordinary small numerical errors could halt the editor otherwise. +//#define FSKELETONPOSER_CHECK_NUMERIC_CODE_ + +#ifdef FSKELETONPOSER_CHECK_NUMERIC_CODE_ + #define FSKELETONPOSER_CHECK_(X,M) checkf(X, M) + #define FSKELETONPOSER_CHECK_FTRANSFORM_EQUALS_(A,B,M) checkf((A).Equals(B), M) + #pragma message ("MIXAMOANIMATIONRETARGETING_CHECK_NUMERIC_CODE_ symbol is defined. Undefine it for redistribution.") +#else + #define FSKELETONPOSER_CHECK_(X,M) + #define FSKELETONPOSER_CHECK_FTRANSFORM_EQUALS_(A,B,M) +#endif + + + +#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule" + + + +int32 FRigConfigurationBoneMapper::MapBoneIndex(int32 BoneIndex) const +{ + const FName SourceBoneName = Source->GetBoneName(BoneIndex); + const FName RigNodeName = SourceSkeleton->GetRigNodeNameFromBoneName(SourceBoneName); + const FName DestinationBoneName = DestinationSkeleton->GetRigBoneMapping(RigNodeName); + const int32 DestinationBoneIndex = Destination->FindBoneIndex(DestinationBoneName); + return DestinationBoneIndex; +} + + + +int32 FEqualNameBoneMapper::MapBoneIndex(int32 BoneIndex) const +{ + const FName SourceBoneName = Source->GetBoneName(BoneIndex); + const int32 DestinationBoneIndex = Destination->FindBoneIndex(SourceBoneName); + return DestinationBoneIndex; +} + + + +FNameTranslationBoneMapper::FNameTranslationBoneMapper( + const FReferenceSkeleton* ASource, + const FReferenceSkeleton* ADestination, + const FStaticNamesMapper & Mapper +) + : FBoneMapper(ASource, ADestination), + NamesMapper(Mapper) +{ +} + + + +int32 FNameTranslationBoneMapper::MapBoneIndex(int32 BoneIndex) const +{ + const FName SourceBoneName = Source->GetBoneName(BoneIndex); + const FName TargetBoneName = MapBoneName(SourceBoneName); + if (!TargetBoneName.IsNone()) + { + const int32 DestinationBoneIndex = Destination->FindBoneIndex(TargetBoneName); + return DestinationBoneIndex; + } + return INDEX_NONE; +} + + + +FName FNameTranslationBoneMapper::MapBoneName(FName BoneName) const +{ + return NamesMapper.MapName(BoneName); +} + + + +FSkeletonPoser::FSkeletonPoser(const USkeleton * Reference, const TArray & ReferenceBonePose) + : ReferenceSkeleton(Reference) +{ + check(ReferenceSkeleton != nullptr); + checkf(ReferenceBonePose.Num() == ReferenceSkeleton->GetReferenceSkeleton().GetNum(), TEXT("Length of bone pose must match the one of the reference skeleton.")); + BoneSpaceToComponentSpaceTransforms(ReferenceSkeleton->GetReferenceSkeleton(), ReferenceBonePose, ReferenceCSBonePoses); +} + + + +void FSkeletonPoser::Pose( + const USkeletalMesh * Mesh, + const FBoneMapper & BoneMapper, + const TArray & PreserveCSBonesNames, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + TArray & MeshBonePose +) const +{ + check(Mesh != nullptr); + + // Convert bone names to bone indices. + TSet PreserveCSBonesIndices; + if (PreserveCSBonesNames.Num() > 0) + { + PreserveCSBonesIndices.Reserve(PreserveCSBonesNames.Num()); + for (const FName & BoneName : PreserveCSBonesNames) + { + const int32 BoneIndex = Mesh->GetRefSkeleton().FindBoneIndex(BoneName); + if (BoneIndex != INDEX_NONE) + { + PreserveCSBonesIndices.Add(BoneIndex); + } + } + } + + TSet> ParentChildBoneIndicesToBypassOneChildConstraint; + ParentChildBoneIndicesToBypassOneChildConstraint.Reserve(ParentChildBoneNamesToBypassOneChildConstraint.Num()); + for (const auto& ParentChildNames : ParentChildBoneNamesToBypassOneChildConstraint) + { + const int32 ParentBoneIndex = Mesh->GetRefSkeleton().FindBoneIndex(ParentChildNames.Get<0>()); + const int32 ChildBoneIndex = Mesh->GetRefSkeleton().FindBoneIndex(ParentChildNames.Get<1>()); + if (ParentBoneIndex != INDEX_NONE && ChildBoneIndex != INDEX_NONE) + { + check(ParentBoneIndex != ChildBoneIndex); + ParentChildBoneIndicesToBypassOneChildConstraint.Add(MakeTuple(ParentBoneIndex, ChildBoneIndex)); + } + } + + UE_LOG(LogMixamoToolkit, Verbose, TEXT("BEGIN: %s -> %s"), *ReferenceSkeleton->GetName(), *Mesh->GetName()); + // NOTE: the RefSkeleton of the Skeletal Mesh counts for its mesh proportions. + Pose(Mesh->GetRefSkeleton(), BoneMapper, PreserveCSBonesIndices, ParentChildBoneIndicesToBypassOneChildConstraint, MeshBonePose); + UE_LOG(LogMixamoToolkit, Verbose, TEXT("END: %s -> %s"), *ReferenceSkeleton->GetName(), *Mesh->GetName()); +} + + + +void FSkeletonPoser::PoseBasedOnRigConfiguration( + const USkeletalMesh * Mesh, + const TArray & PreserveCSBonesNames, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + TArray & MeshBonePose) const +{ + check(Mesh != nullptr); + Pose(Mesh, FRigConfigurationBoneMapper(& Mesh->GetRefSkeleton(), Mesh->GetSkeleton(), & ReferenceSkeleton->GetReferenceSkeleton(), ReferenceSkeleton), PreserveCSBonesNames, ParentChildBoneNamesToBypassOneChildConstraint, MeshBonePose); +} + + + +void FSkeletonPoser::PoseBasedOnCommonBoneNames( + const USkeletalMesh * Mesh, + const TArray & PreserveCSBonesNames, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + TArray & MeshBonePose) const +{ + check(Mesh != nullptr); + Pose(Mesh, FEqualNameBoneMapper(& Mesh->GetRefSkeleton(), & ReferenceSkeleton->GetReferenceSkeleton()), PreserveCSBonesNames, ParentChildBoneNamesToBypassOneChildConstraint, MeshBonePose); +} + + + +void FSkeletonPoser::PoseBasedOnMappedBoneNames( + const USkeletalMesh* Mesh, + const TArray& PreserveCSBonesNames, + const FStaticNamesMapper & SourceToDest_BonesNameMapping, + const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, + TArray & MeshBonePose +) const +{ + check(Mesh != nullptr); + Pose(Mesh, FNameTranslationBoneMapper(& Mesh->GetRefSkeleton(), & ReferenceSkeleton->GetReferenceSkeleton(), SourceToDest_BonesNameMapping), PreserveCSBonesNames, ParentChildBoneNamesToBypassOneChildConstraint, MeshBonePose); +} + + +TArray GetBreadthFirstSortedBones(const FReferenceSkeleton& Skeleton) +{ + const int32 NumBones = Skeleton.GetNum(); + + TArray SortedIndices; + SortedIndices.Reserve(NumBones); + + for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex) + { + SortedIndices.Add(BoneIndex); + } + + SortedIndices.Sort([&](int32 IndexA, int32 IndexB) { + return Skeleton.GetDepthBetweenBones(IndexA, 0) < Skeleton.GetDepthBetweenBones(IndexB, 0); + }); + + return SortedIndices; +} + + +void FSkeletonPoser::Pose( + const FReferenceSkeleton & EditRefSkeleton, + const FBoneMapper & BoneMapper, + const TSet & PreserveCSBonesIndices, + const TSet>& ParentChildBoneIndicesToBypassOneChildConstraint, + TArray & EditBonePoses +) const +{ + check(ReferenceSkeleton != nullptr); + // NOTE: ReferenceSkeleton is used only to get hierarchical infos. + const FReferenceSkeleton & ReferenceRefSkeleton = ReferenceSkeleton->GetReferenceSkeleton(); + + const int32 NumBones = EditRefSkeleton.GetNum(); + EditBonePoses = EditRefSkeleton.GetRefBonePose(); + check(EditBonePoses.Num() == NumBones); + + TArray EditChildrens; + NumOfChildren(EditRefSkeleton, EditChildrens); + TArray OriginalEditCSBonePoses; + if (PreserveCSBonesIndices.Num () > 0) + { + BoneSpaceToComponentSpaceTransforms(EditRefSkeleton, EditRefSkeleton.GetRefBonePose(), OriginalEditCSBonePoses); + } + + //UE_LOG(LogMixamoToolkit, Verbose, TEXT("Initial pose")); + //LogReferenceSkeleton(EditRefSkeleton, EditRefSkeleton.GetRefBonePose()); + auto SortedIndices = GetBreadthFirstSortedBones(EditRefSkeleton); + for (int32 EditBoneIndex : SortedIndices) + { + UE_LOG(LogMixamoToolkit, Verbose, TEXT("Processing bone %d (%s)"), EditBoneIndex, *EditRefSkeleton.GetBoneName(EditBoneIndex).ToString()); + + FVector ReferenceCSBoneOrientation; + if (PreserveCSBonesIndices.Contains(EditBoneIndex)) + { + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Preserving its Component-Space orientation")); + + // Compute orientation of reference bone, considering the original CS bone poses as reference. + const int32 ReferenceBoneParentIndex = EditRefSkeleton.GetParentIndex(EditBoneIndex); + check(ReferenceBoneParentIndex < EditBoneIndex && "Parent bone must have lower index"); + const FTransform & ReferenceCSParentTransform = (ReferenceBoneParentIndex != INDEX_NONE ? OriginalEditCSBonePoses[ReferenceBoneParentIndex] : FTransform::Identity); + const FTransform & ReferenceCSTransform = OriginalEditCSBonePoses[EditBoneIndex]; + ReferenceCSBoneOrientation = (ReferenceCSTransform.GetLocation() - ReferenceCSParentTransform.GetLocation()).GetSafeNormal(); + } + else + { + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Re-posing it")); + + // Get the retarget bone on the reference skeleton. + const int32 ReferenceBoneIndex = BoneMapper.MapBoneIndex(EditBoneIndex); + if (ReferenceBoneIndex == INDEX_NONE) + { + // Bone not retargeted, skip. + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Skipped: not found the corresponding bone in the reference skeleton")); + continue; + } + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Corresponding bone in the reference skeleton: %d (%s)"), ReferenceBoneIndex, *ReferenceRefSkeleton.GetBoneName(ReferenceBoneIndex).ToString()); + + // Compute orientation of reference bone. + const int32 ReferenceBoneParentIndex = ReferenceRefSkeleton.GetParentIndex(ReferenceBoneIndex); + check(ReferenceBoneParentIndex < ReferenceBoneIndex && "Parent bone must have lower index"); + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Parent bone: %d (%s)"), ReferenceBoneParentIndex, ReferenceBoneParentIndex != INDEX_NONE ? *ReferenceRefSkeleton.GetBoneName(ReferenceBoneParentIndex).ToString() : TEXT("-")); + const FTransform & ReferenceCSParentTransform = (ReferenceBoneParentIndex != INDEX_NONE ? ReferenceCSBonePoses[ReferenceBoneParentIndex] : FTransform::Identity); + const FTransform & ReferenceCSTransform = ReferenceCSBonePoses[ReferenceBoneIndex]; + ReferenceCSBoneOrientation = (ReferenceCSTransform.GetLocation() - ReferenceCSParentTransform.GetLocation()).GetSafeNormal(); + // Skip degenerated bones. + if (ReferenceCSBoneOrientation.IsNearlyZero()) + { + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Skipped: degenerate bone orientation in the reference skeleton")); + continue; + } + } + + // Compute current orientation of the bone to retarget (skeleton). + const int32 EditBoneParentIndex = EditRefSkeleton.GetParentIndex(EditBoneIndex); + check(EditBoneParentIndex < EditBoneIndex && "Parent bone must have been already retargeted"); + if (EditBoneParentIndex == INDEX_NONE) + { + // We must rotate the parent bone, but it doesn't exist. Skip. + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Skipped: no parent bone")); + continue; + } + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Parent bone: %d (%s)"), EditBoneParentIndex, *EditRefSkeleton.GetBoneName(EditBoneParentIndex).ToString()); + + if (EditChildrens[EditBoneParentIndex] > 1 && + !ParentChildBoneIndicesToBypassOneChildConstraint.Contains(MakeTuple(EditBoneParentIndex, EditBoneIndex))) + { + // If parent bone has multiple children, modifying it here would ruin the sibling bones. Skip. [NOTE: this bone will differ from the expected result!] + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Skipped: bone %d (%s) not re-oriented because its parent bone (%d - %s) controls also other bones"), EditBoneIndex, *EditRefSkeleton.GetBoneName(EditBoneIndex).ToString(), EditBoneParentIndex, *EditRefSkeleton.GetBoneName(EditBoneParentIndex).ToString()); + continue; + } + // Compute the transforms on the up-to-date skeleton (they cant' be cached). + const FTransform EditCSParentTransform = ComputeComponentSpaceTransform(EditRefSkeleton, EditBonePoses, EditBoneParentIndex); + const FTransform EditCSTransform = EditBonePoses[EditBoneIndex] * EditCSParentTransform; + const FVector EditCSBoneOrientation = (EditCSTransform.GetLocation() - EditCSParentTransform.GetLocation()).GetSafeNormal(); + + // Skip degenerated or already-aligned bones. + if (EditCSBoneOrientation.IsNearlyZero() || ReferenceCSBoneOrientation.Equals(EditCSBoneOrientation)) + { + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Skipped: degenerate or already-aligned bone")); + continue; + } + + // Delta rotation (in Component Space) to make the skeleton bone aligned to the reference one. + const FQuat EditToReferenceCSRotation = FQuat::FindBetweenVectors(EditCSBoneOrientation, ReferenceCSBoneOrientation); + FSKELETONPOSER_CHECK_( + EditToReferenceCSRotation.RotateVector(EditCSBoneOrientation).Equals(ReferenceCSBoneOrientation), + TEXT("The rotation applied to the Edited Bone orientation must match the Reference one, in Component Space") + ); + // Convert from Component Space to skeleton Bone Space + const FQuat EditToReferenceBSRotation = EditCSParentTransform.GetRotation().Inverse() * EditToReferenceCSRotation * EditCSParentTransform.GetRotation(); + +#if defined(FSKELETONPOSER_CHECK_NUMERIC_CODE_) && DO_CHECK + const FTransform & EditParentRefBonePose = EditRefSkeleton.GetRefBonePose()[EditBoneParentIndex]; +#endif + FSKELETONPOSER_CHECK_FTRANSFORM_EQUALS_( + EditBonePoses[EditBoneParentIndex], + EditParentRefBonePose, + TEXT("Bone pose transform is still the same as the original one") + ); + + // Apply the rotation to the *parent* bone (yep!!!) + EditBonePoses[EditBoneParentIndex].ConcatenateRotation(EditToReferenceBSRotation); + +#if defined(FSKELETONPOSER_CHECK_NUMERIC_CODE_) && DO_CHECK + { + const FTransform NewSkeletonCSParentTransform = ComputeComponentSpaceTransform(EditRefSkeleton, EditBonePoses, EditBoneParentIndex); + // For some reasons, check on thumbs need a much higher tollerance (thumb_02_l, thumb_03_l, thumb_02_r, thumb_03_r). + FSKELETONPOSER_CHECK_( + ((EditBonePoses[EditBoneIndex] * NewSkeletonCSParentTransform).GetLocation() - NewSkeletonCSParentTransform.GetLocation()).GetSafeNormal().Equals(ReferenceCSBoneOrientation, 1e-3), + TEXT("The new Bone pose results now in the same orientation as the reference one") + ); + } +#endif + FSKELETONPOSER_CHECK_FTRANSFORM_EQUALS_( + EditBonePoses[EditBoneParentIndex], + FTransform(EditToReferenceBSRotation) * EditParentRefBonePose, + TEXT("Using ConcatenateRotation() is the same as pre-multiplying with the delta rotation") + ); + UE_LOG(LogMixamoToolkit, Verbose, TEXT(" Done: changed parent bone %d (%s): %s"), EditBoneParentIndex, *EditRefSkeleton.GetBoneName(EditBoneParentIndex).ToString(), *EditBonePoses[EditBoneParentIndex].ToString()); + + // Notes. + // + // FTransform uses the VQS notation: S->Q->V (where S = scale, Q = rotation, V = translation; considering each of them as affine transforms: S * Q * V). + // + // FQuat multiples in the opposite order, i.e. Q*Q' corresponds to q'*q. + // + // SetRotation() - used by FIKRetargetPose - changes Q, in the middle of the S*Q*V. + // Let's consider Q and Q' the FTransform corresponding to the quaternions q and q', + // after SetRotation(Q*Q') the corresponding FTransform is: + // + // S * (Q * Q') * V = S * Q * V * V(-1) * Q' * V + // + // i.e. it's equal to the original FTransform S*Q*V post-multiplied by V(-1)*Q'*V. + } + //UE_LOG(LogMixamoToolkit, Verbose, TEXT("Retargeted pose")); + //LogReferenceSkeleton(EditRefSkeleton, EditBonePoses); +} + + + +void FSkeletonPoser::ApplyPoseToRetargetBasePose(USkeletalMesh* Mesh, const TArray& MeshBonePose) +{ + checkf(Mesh->GetRetargetBasePose().Num() == MeshBonePose.Num(), TEXT("Computed pose must have the same number of transforms as the target retarget base pose")); + // We'll change RetargetBasePose. + Mesh->Modify(); + // Transforms computed by FSkeletonPoser::Pose() are already compatible with RetargetBasePose, a simple assignment is enough. + Mesh->GetRetargetBasePose() = MeshBonePose; +} + + + +void FSkeletonPoser::ApplyPoseToIKRetargetPose(USkeletalMesh* Mesh, UIKRetargeterController* Controller, const TArray& MeshBonePose) +{ + check(Controller != nullptr); + + // NOTE: using the FIKRigSkeleton::CurrentPoseLocal (Controller->GetAsset()->GetTargetIKRig()->Skeleton) is wrong + // as it reflects only the Skeletal Mesh used to create the IK Rig asset, and not current input Mesh. + // + // UIKRetargetProcessor::Initialize() calls FRetargetSkeleton::Initialize() that calls FRetargetSkeleton::GenerateRetargetPose(), + // and they re-generate the RetargetLocalPose (corresponding to FIKRigSkeleton::CurrentPoseLocal) + // from SkeletalMesh->GetRefSkeleton().GetRefBonePose() [where SkeletalMesh is the target skeletal mesh] + // + // So we do it also here. + const FReferenceSkeleton& MeshRefSkeleton = Mesh->GetRefSkeleton(); + const TArray & TargetBonePose = Mesh->GetRefSkeleton().GetRefBonePose(); + + const int32 NumBones = MeshBonePose.Num(); + checkf(TargetBonePose.Num() == NumBones, TEXT("Computed pose must have the same number of transforms as in the target IK Rig Skeleton")); + for (int32 EditBoneIndex = 0; EditBoneIndex < NumBones; ++EditBoneIndex) + { + /** + See also the comments in FSkeletonPoser::Pose(). + + FTransform follows the VQS notation: T=S*Q*V. + + IKRigSkeleton.CurrentPoseLocal[EditBoneIndex] is the base pose (=S*R*V) used by IKRigSkeleton + to compute the final pose when considering the Rotation Offset (call it Q). + + MeshBonePose[EditBoneIndex] contains the resulting pose with the added Rotation Offset Q (= S*R'*V = S*(Q*R)*V). + + R' = Q * R + R' * R(-1) = Q + + as quaternions, and considering that FQuat applies multiplications in reverse order: + + r(-1) * r' = q + */ + FQuat Q = TargetBonePose[EditBoneIndex].GetRotation().Inverse() * MeshBonePose[EditBoneIndex].GetRotation(); + Controller->SetRotationOffsetForRetargetPoseBone(MeshRefSkeleton.GetBoneName(EditBoneIndex), Q, ERetargetSourceOrTarget::Target); + +#if defined(FSKELETONPOSER_CHECK_NUMERIC_CODE_) && DO_CHECK + { + // This is how the FIKRetargetPose computes the final bone poses (calling SetRotation()). + FTransform IKRes = TargetBonePose[EditBoneIndex]; + IKRes.SetRotation(Q * IKRes.GetRotation()); + FSKELETONPOSER_CHECK_FTRANSFORM_EQUALS_( + MeshBonePose[EditBoneIndex], + IKRes, + TEXT("The computed FIKRetargetPose pose must match the computed mesh bone pose") + ); + } +#endif + } +} + + + +FTransform FSkeletonPoser::ComputeComponentSpaceTransform(const FReferenceSkeleton & RefSkeleton, const TArray & RelTransforms, int32 BoneIndex) +{ + if (BoneIndex == INDEX_NONE) + { + return FTransform::Identity; + } + + FTransform T = RelTransforms[BoneIndex]; + int32 i = RefSkeleton.GetParentIndex(BoneIndex); + while (i != INDEX_NONE) + { + checkf(i < BoneIndex, TEXT("Parent bone must have been already retargeted")); + T *= RelTransforms[i]; + i = RefSkeleton.GetParentIndex(i); + } + + return T; +} + + + +void FSkeletonPoser::BoneSpaceToComponentSpaceTransforms(const FReferenceSkeleton & RefSkeleton, const TArray & BSTransforms, TArray & CSTransforms) +{ + check(RefSkeleton.GetNum() == BSTransforms.Num()); + const int32 NumBones = RefSkeleton.GetNum(); + CSTransforms.Empty(NumBones); + CSTransforms.AddUninitialized(NumBones); + for (int32 iBone = 0; iBone < NumBones; ++iBone) + { + CSTransforms[iBone] = BSTransforms[iBone]; + const int32 iParent = RefSkeleton.GetParentIndex(iBone); + check(iParent < iBone); + if (iParent != INDEX_NONE) + { + CSTransforms[iBone] *= CSTransforms[iParent]; + } + } +} + + + +void FSkeletonPoser::NumOfChildren(const FReferenceSkeleton & RefSkeleton, TArray & children) +{ + const int32 NumBones = RefSkeleton.GetNum(); + children.Empty(NumBones); + children.AddUninitialized(NumBones); + for (int32 iBone = 0; iBone < NumBones; ++iBone) + { + children[iBone] = 0; + const int32 iParent = RefSkeleton.GetParentIndex(iBone); + check(iParent < iBone); + if (iParent != INDEX_NONE) + { + ++children[iParent]; + } + } +} + + + +void FSkeletonPoser::LogReferenceSkeleton (const FReferenceSkeleton & RefSkeleton, const TArray & Poses, int BoneIndex, int Deep) +{ + FString Indent; + for (int i = 0; i < Deep; ++i) + { + Indent.Append(TEXT(" ")); + } + + UE_LOG(LogMixamoToolkit, Verbose, TEXT("%s[%d - %s]: %s"), * Indent, BoneIndex, * RefSkeleton.GetBoneName(BoneIndex).ToString(), * Poses[BoneIndex].ToString ()); + + for (int i = BoneIndex + 1; i < Poses.Num(); ++i) + { + if (RefSkeleton.GetParentIndex(i) == BoneIndex) + { + LogReferenceSkeleton(RefSkeleton, Poses, i, Deep + 1); + } + } +} + + + +#undef LOCTEXT_NAMESPACE diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.h new file mode 100644 index 00000000..d1c24adf --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Private/SkeletonPoser.h @@ -0,0 +1,178 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +#include "Engine/EngineTypes.h" +#include "NamesMapper.h" + + + +class USkeleton; +class USkeletalMesh; +struct FReferenceSkeleton; + + + +/** +Class to map bones from a reference skeleton to another. + +@attention Skeletal meshes can share the same USkeleton asset, but they can have different FReferenceSkeleton data + (with more or less data). + The effective valid bone data (indexes and names) used by a skeletal mesh are the ones stored in its FReferenceSkeleton object. + +@attention To be used within a single method's stack space. +*/ +class FBoneMapper +{ +public: + FBoneMapper(const FReferenceSkeleton * ASource, const FReferenceSkeleton * ADestination) + : Source(ASource), + Destination(ADestination) + {} + + virtual + ~FBoneMapper() + {} + + /** + Map a bone index from the source skeleton to a bone index into the destination skeleton. + + Returns INDEX_NONE if the bone can't be mapped. + */ + virtual + int32 MapBoneIndex(int32 BoneIndex) const = 0; + +protected: + const FReferenceSkeleton * Source; + const FReferenceSkeleton * Destination; +}; + + + +/** +A bone mapper based on FRigConfiguration. + +A bone in the source skeleton is mapped to the bone in the destination skeleton sharing the same "rig node name", +as defined by the FRigConfiguration of the two skeletons (that must be compatible). + +@attention "rig node name" is not the same as "bone name". +*/ +class FRigConfigurationBoneMapper : public FBoneMapper +{ +public: + FRigConfigurationBoneMapper(const FReferenceSkeleton * ASource, const USkeleton * ASourceSkeleton, const FReferenceSkeleton * ADestination, const USkeleton * ADestinationSkeleton) + : FBoneMapper(ASource, ADestination), + SourceSkeleton(ASourceSkeleton), + DestinationSkeleton(ADestinationSkeleton) + {} + + virtual + int32 MapBoneIndex(int32 BoneIndex) const override; + +protected: + const USkeleton * SourceSkeleton; + const USkeleton * DestinationSkeleton; +}; + + + +/** +A bone mapper mapping bones by matching name. + +A bone in the source skeleton is mapped to the bone in the destination skeleton having the same "bone name". +*/ +class FEqualNameBoneMapper : public FBoneMapper +{ +public: + FEqualNameBoneMapper(const FReferenceSkeleton * ASource, const FReferenceSkeleton * ADestination) + : FBoneMapper(ASource, ADestination) + {} + + virtual + int32 MapBoneIndex(int32 BoneIndex) const override; +}; + + + +/** +A bone mapper mapping bones by matching "translated" name. + +A bone in the source skeleton is mapped to the bone in the destination skeleton having the same "translated bone name", +i.e. the source bone name is translated accordingly to a translation map and the resulting bone name is looked for in the destination skeleton. +*/ +class FNameTranslationBoneMapper : public FBoneMapper +{ +public: + /** + @param SourceToDest_BonesNameMapping Array of strings where the 2*i-th string is a bone name of the source skeleton and + (2*i+1)-th string is the translated name in the destination skeleton. + @param SourceToDest_BonesNameMappingNum Length of the array SourceToDest_BonesNameMapping. + @param Reverse If the mapping table must be applied in reverse, i.e. mapping from (2*i+1) to 2*i (instead of the default 2*i -> 2*i+1). + Set to true if SourceToDest_BonesNameMapping is mapping bones from the Destination skeleton to the Source skeleton, instead of the expected "Source to Destination". + */ + FNameTranslationBoneMapper(const FReferenceSkeleton* ASource, const FReferenceSkeleton* ADestination, const FStaticNamesMapper & Mapper); + + virtual + int32 MapBoneIndex(int32 BoneIndex) const override; + + FName MapBoneName(FName BoneName) const; + +private: + FStaticNamesMapper NamesMapper; +}; + + + +/** +Class to compute a matching pose from one skeleton to another, distinct one. + +@attention To be used within a single method's stack space. +*/ +class FSkeletonPoser +{ +public: + /** + @param Reference the Reference Skeleton used by the poser, i.e. the skeleton that we want to "reproduce" + @param ReferenceBonePose the pose that we want to reproduce in other skeletons. + This is an array of transforms in Bone Space, following the order and hierarchy as in Reference->GetReferenceSkeleton(). + */ + FSkeletonPoser(const USkeleton * Reference, const TArray & ReferenceBonePose); + /** + Compute the matching pose for a given Skeletal Mesh. + + @param Mesh The skeletal mesh for which compute a pose, matching the Reference Pose configured in the constructor. + @param BoneMapper A bone mapper converting bones used by Mesh into bones of the Reference skeleton. + @param PreserveCSBonesNames A set of bone names of Mesh for which the Component Space transform (relative to the parent) must be preserved. + @param ParentChildBoneNamesToBypassOneChildConstraint A set of parent-child bone names of Mesh that must be forcefully oriented regardless of + the children number of the parent bone. + @param MeshBonePose In output it will contain the resulting computed matching pose for Mesh. + This is an array of transforms in Bone Space, following the order and hierarchy as in Mesh->GetRefSkeleton(). + */ + void Pose(const USkeletalMesh * Mesh, const FBoneMapper & BoneMapper, const TArray & PreserveCSBonesNames, const TArray> & ParentChildBoneNamesToBypassOneChildConstraint, TArray & MeshBonePose) const; + + // Utility methods. + void PoseBasedOnRigConfiguration(const USkeletalMesh * Mesh, const TArray & PreserveCSBonesNames, const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, TArray & MeshBonePose) const; + void PoseBasedOnCommonBoneNames(const USkeletalMesh * Mesh, const TArray & PreserveCSBonesNames, const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, TArray & MeshBonePose) const; + void PoseBasedOnMappedBoneNames(const USkeletalMesh * Mesh, const TArray & PreserveCSBonesNames, const FStaticNamesMapper & SourceToDest_BonesNameMapping, const TArray>& ParentChildBoneNamesToBypassOneChildConstraint, TArray & MeshBonePose) const; + + // Utility methods + static void ApplyPoseToRetargetBasePose(USkeletalMesh* Mesh, const TArray& MeshBonePose); + static void ApplyPoseToIKRetargetPose(USkeletalMesh* Mesh, UIKRetargeterController* Controller, const TArray& MeshBonePose); + +private: + const USkeleton * ReferenceSkeleton; + TArray ReferenceCSBonePoses; + +private: + void Pose(const FReferenceSkeleton & EditRefSkeleton, const FBoneMapper & BoneMapper, const TSet & PreserveCSBonesIndices, const TSet>& ParentChildBoneIndicesToBypassOneChildConstraint, TArray & MeshBonePoses) const; + + static + FTransform ComputeComponentSpaceTransform(const FReferenceSkeleton & RefSkeleton, const TArray & RelTransforms, int32 BoneIndex); + static + void BoneSpaceToComponentSpaceTransforms(const FReferenceSkeleton & RefSkeleton, const TArray & BSTransforms, TArray & CSTransforms); + static + void NumOfChildren(const FReferenceSkeleton & RefSkeleton, TArray & children); + + static + void LogReferenceSkeleton(const FReferenceSkeleton & RefSkeleton, const TArray & Poses, int BoneIndex = 0, int Deep = 0); +}; diff --git a/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Public/MixamoAnimationRetargeting.h b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Public/MixamoAnimationRetargeting.h new file mode 100644 index 00000000..eac0020b --- /dev/null +++ b/EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Source/MixamoAnimationRetargeting/Public/MixamoAnimationRetargeting.h @@ -0,0 +1,5 @@ +// Copyright 2022 UNAmedia. All Rights Reserved. + +#pragma once + +// No public module interface. \ No newline at end of file