Add Mixamo Animation Retargeter Plugin

This commit is contained in:
Philip W 2024-02-13 19:11:13 +00:00
parent 9ea72a861f
commit 81710f9b74
28 changed files with 5068 additions and 0 deletions

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

Binary file not shown.

Binary file not shown.

View File

@ -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;
}
}

View File

@ -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<FMixamoAnimationRetargetingModule>(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<FMessageLogModule>("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<FMixamoSkeletonRetargeter> FMixamoAnimationRetargetingModule::GetMixamoSkeletonRetargeter()
{
return MixamoSkeletonRetargeter.ToSharedRef();
}
TSharedRef<FMixamoAnimationRootMotionSolver> FMixamoAnimationRetargetingModule::GetMixamoAnimationRootMotionSolver()
{
return MixamoAnimationRootMotionSolver.ToSharedRef();
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FMixamoAnimationRetargetingModule, MixamoAnimationRetargeting)

View File

@ -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<SWindow> WidgetWindow = SNew(SWindow)
.Title(LOCTEXT("FMixamoAnimationRootMotionSolver_AskUserForAnimations_WindowTitle", "Select animations"))
.ClientSize(FVector2D(1000, 600))
.SupportsMinimize(false)
.SupportsMaximize(false)
.HasCloseButton(false);
TSharedRef<SRootMotionExtractionWidget> 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<FAssetToolsModule>(NAME_AssetTools).Get();
const FString ResultAnimationName = SelectedAnimation->GetName() + "_rootmotion";
const FString PackagePath = FAssetData(SelectedAnimation).PackagePath.ToString();
UAnimSequence* ResultAnimation = Cast<UAnimSequence>(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<FContentBrowserModule>("ContentBrowser");
TArray<UObject*> 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;
}

View File

@ -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);
};

View File

@ -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<USkeleton *> 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<USkeletalMesh *> SkeletalMeshes) const;
void AddRootBone(const USkeleton * Skeleton, FReferenceSkeleton * RefSkeleton) const;
void SetupTranslationRetargetingModes(USkeleton* Skeleton) const;
void RetargetBasePose(
TArray<USkeletalMesh *> SkeletalMeshes,
const USkeleton * ReferenceSkeleton,
const TArray<FName>& PreserveCSBonesNames,
const FStaticNamesMapper & EditToReference_BoneNamesMapping,
const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint,
bool bApplyPoseToRetargetBasePose,
class UIKRetargeterController* Controller
) const;
USkeleton * AskUserForTargetSkeleton() const;
bool AskUserOverridingAssetsConfirmation(const TArray<UObject*>& AssetsToOverwrite) const;
/// Valid within a single method's stack space.
void GetAllSkeletalMeshesUsingSkeleton(const USkeleton * Skeleton, TArray<FAssetData> & SkeletalMeshes) const;
void GetAllSkeletalMeshesUsingSkeleton(const USkeleton * Skeleton, TArray<USkeletalMesh *> & SkeletalMeshes) const;
void SetPreviewMesh(USkeleton * Skeleton, USkeletalMesh * PreviewMesh) const;
void EnumerateAssetsToOverwrite(const USkeleton* Skeleton, const USkeleton* ReferenceSkeleton, TArray<UObject*>& 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<FName> & TargetBoneChainsToSkip,
const TArray<FName> & TargetBoneChainsDriveIKGoal,
const TArray<FName>& 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;
};

View File

@ -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<FMixamoToolkitCommands>(
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

View File

@ -0,0 +1,21 @@
// Copyright 2022 UNAmedia. All Rights Reserved.
#pragma once
#include "Framework/Commands/Commands.h"
class FMixamoToolkitCommands : public TCommands<FMixamoToolkitCommands>
{
public:
FMixamoToolkitCommands();
// TCommands<> interface
virtual void RegisterCommands() override;
public:
//TSharedPtr< FUICommandInfo > OpenBatchConverterWindow;
TSharedPtr< FUICommandInfo > RetargetMixamoSkeleton;
TSharedPtr< FUICommandInfo > ExtractRootMotion;
};

View File

@ -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<FContentBrowserModule>("ContentBrowser");
TArray<FContentBrowserMenuExtender_SelectedAssets> & 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<void()> 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<USkeleton *> Skeletons;
for (const FAssetData& Asset : ContentBrowserSelectedAssets)
{
if (CanExecuteAction_RetargetMixamoSkeleton(Asset))
{
USkeleton * o = CastChecked<USkeleton> (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<USkeleton> (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<USkeleton>(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<USkeleton>(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<FExtender> FMixamoToolkitEditorIntegration::MakeContentBrowserContextMenuExtender(const TArray<FAssetData> & NewSelectedAssets)
{
ContentBrowserSelectedAssets = NewSelectedAssets;
TSharedRef<FExtender> 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<void()> RetargetMixamoSkeletonsFunction = [this]() { ExecuteAction_RetargetMixamoSkeletons(); };
// Add the RetargetMixamoSkeleton action.
TSharedRef< FUICommandInfo > cmd = FMixamoToolkitCommands::Get().RetargetMixamoSkeleton.ToSharedRef();
MenuBuilder.AddMenuEntry(
cmd->GetLabel(),
TAttribute<FText>::CreateSP(this, &FMixamoToolkitEditorIntegration::TooltipGetter_RetargetMixamoSkeletons),
cmd->GetIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::ExecuteChecked, RetargetMixamoSkeletonsFunction),
FCanExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::CanExecuteAction_RetargetMixamoSkeletons)
)
);
TFunction<void()> ExtractRootMotionFunction = [this]() { ExecuteAction_ExtractRootMotion(); };
// Add the ExtractRootMotion action.
cmd = FMixamoToolkitCommands::Get().ExtractRootMotion.ToSharedRef();
MenuBuilder.AddMenuEntry(
cmd->GetLabel(),
TAttribute<FText>::CreateSP(this, &FMixamoToolkitEditorIntegration::TooltipGetter_ExtractRootMotion),
cmd->GetIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::ExecuteChecked, ExtractRootMotionFunction),
FCanExecuteAction::CreateSP(this, &FMixamoToolkitEditorIntegration::CanExecuteAction_ExtractRootMotion)
)
);
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,43 @@
// Copyright 2022 UNAmedia. All Rights Reserved.
#pragma once
#include <Engine/EngineBaseTypes.h>
class FMixamoToolkitEditorIntegration : public TSharedFromThis<FMixamoToolkitEditorIntegration>
{
public:
void Register();
void Unregister();
private:
TSharedPtr<class FUICommandList> PluginCommands;
// Store currently selected assets from Content Browser here to avoid passing them in lambda closures.
TArray<FAssetData> ContentBrowserSelectedAssets;
private:
// Tooltips.
FText TooltipGetter_RetargetMixamoSkeletons() const;
FText TooltipGetter_ExtractRootMotion() const;
private:
void ExecuteChecked(TFunction<void()> 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<class FExtender> MakeContentBrowserContextMenuExtender(const TArray<FAssetData> & NewSelectedAssets);
void AddContentBrowserContextSubMenu(class FMenuBuilder& MenuBuilder) const;
void AddContentBrowserContextMenuEntries(class FMenuBuilder& MenuBuilder) const;
};

View File

@ -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<FMixamoAnimationRetargetingModule>
{
public:
static
FMixamoAnimationRetargetingModule & Get();
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
TSharedRef<class FMixamoSkeletonRetargeter> GetMixamoSkeletonRetargeter();
TSharedRef<class FMixamoAnimationRootMotionSolver> GetMixamoAnimationRootMotionSolver();
private:
TSharedPtr<class FMixamoSkeletonRetargeter> MixamoSkeletonRetargeter;
TSharedPtr<class FMixamoAnimationRootMotionSolver> MixamoAnimationRootMotionSolver;
TSharedPtr<class FMixamoToolkitEditorIntegration> EditorIntegration;
};

View File

@ -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

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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<typename TTableItem, typename TFunctor>
void IterateOnMappingTable(const TTableItem * Table, int32 TableNum, int32 iColumn, TFunctor Functor)
{
for (int32 i = 0; i < TableNum; i += 2)
{
Functor(Table[i + iColumn]);
}
}
template<typename TTableItem, typename TContainer>
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<FName> FStaticNamesMapper::MapNames(const TArray<FName>& Names) const
{
TArray<FName> 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<FName>& Out) const
{
GetMappingTableColumn(Mapping, MappingNum, 0, Out);
}
void FStaticNamesMapper::GetDestination(TArray<FName>& Out) const
{
GetMappingTableColumn(Mapping, MappingNum, 1, Out);
}

View File

@ -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<FName> MapNames(const TArray<FName>& Names) const;
FStaticNamesMapper GetInverseMapper() const;
void GetSource(TArray<FName>& Out) const;
void GetDestination(TArray<FName>& Out) const;
protected:
const char* const* Mapping;
int32 MappingNum;
int32 SrcOfs;
int32 DstOfs;
};

View File

@ -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<UObject::FAssetRegistryTag> AssetRegistryTags;
USkeleton::StaticClass()->GetDefaultObject()->GetAssetRegistryTags(AssetRegistryTags);
for (UObject::FAssetRegistryTag & AssetRegistryTag : AssetRegistryTags)
{
AssetPickerConfig.HiddenColumnNames.Add(AssetRegistryTag.Name.ToString());
}
FContentBrowserModule & ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
TSharedRef<SWidget> 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<USkeleton>(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<SWindow> 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<SWidget> SRootMotionExtractionWidget::CreateAnimationSequencePicker(const USkeleton * ReferenceSkeleton, bool InPlaceAnimation)
{
auto OnClickDelegate = [this, InPlaceAnimation](const FAssetData & AssetData)
{
UAnimSequence* anim = Cast<UAnimSequence>(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<UObject::FAssetRegistryTag> AssetRegistryTags;
UAnimSequence::StaticClass()->GetDefaultObject()->GetAssetRegistryTags(AssetRegistryTags);
for (UObject::FAssetRegistryTag & AssetRegistryTag : AssetRegistryTags)
{
AssetPickerConfig.HiddenColumnNames.Add(AssetRegistryTag.Name.ToString());
}
FContentBrowserModule & ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("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<SWindow> 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<SWindow> window = FSlateApplication::Get().FindWidgetWindow(AsShared());
if (window.IsValid())
{
window->RequestDestroyWindow();
}
}
bool SOverridingAssetsConfirmationDialog::EnumerateCustomSourceItemDatas(TFunctionRef<bool(FContentBrowserItemData&&)> InCallback)
{
UContentBrowserDataSubsystem* ContentBrowserDataSubsystem = IContentBrowserDataModule::Get().GetSubsystem();
#if 0
TArray<FContentBrowserItemPath> 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<UContentBrowserDataSource*>(ModularFeatures.GetModularFeatureImplementation(BrowserDataSourceTypeName, ExtensionIndex));
if (DataSource->IsA<UContentBrowserAssetDataSource>())
{
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<FSourcesData>();
// 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<SAssetView> 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

View File

@ -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<SWidget> 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<UObject*>, 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<bool(FContentBrowserItemData&&)> InCallback);
private:
TArray<UObject*> AssetsToOverwrite;
bool bConfirmed;
};

View File

@ -0,0 +1,49 @@
// Copyright 2022 UNAmedia. All Rights Reserved.
#include "SkeletonMatcher.h"
#include <Animation/Skeleton.h>
#define LOCTEXT_NAMESPACE "FMixamoAnimationRetargetingModule"
FSkeletonMatcher::FSkeletonMatcher(
const TArray<FName>& 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

View File

@ -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<FName> & InBoneNames, float InMinimumMatchingPerc);
bool IsMatching(const USkeleton * Skeleton) const;
private:
const TArray<FName> BoneNames;
const float MinimumMatchingPerc;
};

View File

@ -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 <RetargetEditor/IKRetargeterController.h>
// 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<FTransform> & 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<FName> & PreserveCSBonesNames,
const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint,
TArray<FTransform> & MeshBonePose
) const
{
check(Mesh != nullptr);
// Convert bone names to bone indices.
TSet<int32> 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<TPair<int32, int32>> 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<FName> & PreserveCSBonesNames,
const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint,
TArray<FTransform> & 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<FName> & PreserveCSBonesNames,
const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint,
TArray<FTransform> & MeshBonePose) const
{
check(Mesh != nullptr);
Pose(Mesh, FEqualNameBoneMapper(& Mesh->GetRefSkeleton(), & ReferenceSkeleton->GetReferenceSkeleton()), PreserveCSBonesNames, ParentChildBoneNamesToBypassOneChildConstraint, MeshBonePose);
}
void FSkeletonPoser::PoseBasedOnMappedBoneNames(
const USkeletalMesh* Mesh,
const TArray<FName>& PreserveCSBonesNames,
const FStaticNamesMapper & SourceToDest_BonesNameMapping,
const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint,
TArray<FTransform> & MeshBonePose
) const
{
check(Mesh != nullptr);
Pose(Mesh, FNameTranslationBoneMapper(& Mesh->GetRefSkeleton(), & ReferenceSkeleton->GetReferenceSkeleton(), SourceToDest_BonesNameMapping), PreserveCSBonesNames, ParentChildBoneNamesToBypassOneChildConstraint, MeshBonePose);
}
TArray<int32> GetBreadthFirstSortedBones(const FReferenceSkeleton& Skeleton)
{
const int32 NumBones = Skeleton.GetNum();
TArray<int32> 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<int32> & PreserveCSBonesIndices,
const TSet<TPair<int32, int32>>& ParentChildBoneIndicesToBypassOneChildConstraint,
TArray<FTransform> & 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<int> EditChildrens;
NumOfChildren(EditRefSkeleton, EditChildrens);
TArray<FTransform> 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<FTransform>& 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<FTransform>& 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<FTransform> & 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<FTransform> & 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<FTransform> & BSTransforms, TArray<FTransform> & 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<int> & 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<FTransform> & 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

View File

@ -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<FTransform> & 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<FName> & PreserveCSBonesNames, const TArray<TPair<FName, FName>> & ParentChildBoneNamesToBypassOneChildConstraint, TArray<FTransform> & MeshBonePose) const;
// Utility methods.
void PoseBasedOnRigConfiguration(const USkeletalMesh * Mesh, const TArray<FName> & PreserveCSBonesNames, const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint, TArray<FTransform> & MeshBonePose) const;
void PoseBasedOnCommonBoneNames(const USkeletalMesh * Mesh, const TArray<FName> & PreserveCSBonesNames, const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint, TArray<FTransform> & MeshBonePose) const;
void PoseBasedOnMappedBoneNames(const USkeletalMesh * Mesh, const TArray<FName> & PreserveCSBonesNames, const FStaticNamesMapper & SourceToDest_BonesNameMapping, const TArray<TPair<FName, FName>>& ParentChildBoneNamesToBypassOneChildConstraint, TArray<FTransform> & MeshBonePose) const;
// Utility methods
static void ApplyPoseToRetargetBasePose(USkeletalMesh* Mesh, const TArray<FTransform>& MeshBonePose);
static void ApplyPoseToIKRetargetPose(USkeletalMesh* Mesh, UIKRetargeterController* Controller, const TArray<FTransform>& MeshBonePose);
private:
const USkeleton * ReferenceSkeleton;
TArray<FTransform> ReferenceCSBonePoses;
private:
void Pose(const FReferenceSkeleton & EditRefSkeleton, const FBoneMapper & BoneMapper, const TSet<int32> & PreserveCSBonesIndices, const TSet<TPair<int32, int32>>& ParentChildBoneIndicesToBypassOneChildConstraint, TArray<FTransform> & MeshBonePoses) const;
static
FTransform ComputeComponentSpaceTransform(const FReferenceSkeleton & RefSkeleton, const TArray<FTransform> & RelTransforms, int32 BoneIndex);
static
void BoneSpaceToComponentSpaceTransforms(const FReferenceSkeleton & RefSkeleton, const TArray<FTransform> & BSTransforms, TArray<FTransform> & CSTransforms);
static
void NumOfChildren(const FReferenceSkeleton & RefSkeleton, TArray<int> & children);
static
void LogReferenceSkeleton(const FReferenceSkeleton & RefSkeleton, const TArray<FTransform> & Poses, int BoneIndex = 0, int Deep = 0);
};

View File

@ -0,0 +1,5 @@
// Copyright 2022 UNAmedia. All Rights Reserved.
#pragma once
// No public module interface.