Add Mixamo Animation Retargeter Plugin
This commit is contained in:
parent
9ea72a861f
commit
81710f9b74
@ -33,6 +33,11 @@
|
|||||||
"Name": "VaultIt",
|
"Name": "VaultIt",
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/7d19b5622d6846edb9881e6c89bce05e"
|
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/7d19b5622d6846edb9881e6c89bce05e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "MixamoAnimationRetargeting",
|
||||||
|
"Enabled": true,
|
||||||
|
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/c684998124da4e2583b314dc95403a80"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Content/Mixamo/CR_Mixamo_BasicFootIK.uasset
(Stored with Git LFS)
Normal file
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Content/Mixamo/CR_Mixamo_BasicFootIK.uasset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/ButtonIcon_40x.png
(Stored with Git LFS)
Normal file
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/ButtonIcon_40x.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/Icon128.png
(Stored with Git LFS)
Normal file
BIN
EndlessVendetta/Plugins/MixamoAnimationRetargeting2/Resources/Icon128.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
};
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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;
|
||||||
|
};
|
@ -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
|
@ -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);
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright 2022 UNAmedia. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// No public module interface.
|
Loading…
Reference in New Issue
Block a user