Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/SkeletalMeshModifiers/Private/SkeletonModifier.cpp
2025-05-18 13:04:45 +08:00

2102 lines
63 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SkeletonModifier.h"
#include "AssetNotifications.h"
#include "BoneWeights.h"
#include "FileHelpers.h"
#include "MeshDescription.h"
#include "Animation/Skeleton.h"
#include "ReferenceSkeleton.h"
#include "SkeletalMeshAttributes.h"
#include "Animation/AnimSequence.h"
#include "Animation/PoseAsset.h"
#include "Animation/AnimMontage.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/AssetData.h"
#include "Engine/SkeletalMesh.h"
#include "RenderingThread.h"
#include "Animation/MirrorDataTable.h"
#include "PropertyEditorModule.h"
#include "Dialog/SCustomDialog.h"
#include "Misc/MessageDialog.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Text/STextBlock.h"
namespace USkeletonModifierLocals
{
static constexpr int32 LODIndex = 0;
/**
* FReferenceSkeletonCompatibilityChecker is a utility class to check whether two skeletons are compatible.
* It follows the USkeleton compatibility pattern.
* NOTE: there should be consistency between this, USkeleton and FSkeletonHelper so it should be merged at some point
*/
class FReferenceSkeletonCompatibilityChecker
{
public:
FReferenceSkeletonCompatibilityChecker(const FReferenceSkeleton& InRefSkeleton)
: ReferenceSkeleton(InRefSkeleton)
{}
bool DoesParentChainMatch(const int32 InStartBoneIndex, const FReferenceSkeleton& InRefSkeleton) const
{
// if start is root bone
if ( InStartBoneIndex == 0 )
{
// verify name of root bone matches
return (ReferenceSkeleton.GetBoneName(0) == InRefSkeleton.GetBoneName(0));
}
int32 SkeletonBoneIndex = InStartBoneIndex;
// If skeleton bone is not found in mesh, fail.
int32 OtherBoneIndex = InRefSkeleton.FindBoneIndex( ReferenceSkeleton.GetBoneName(SkeletonBoneIndex) );
if( OtherBoneIndex == INDEX_NONE )
{
return false;
}
do
{
// verify if parent name matches
int32 ParentSkeletonBoneIndex = ReferenceSkeleton.GetParentIndex(SkeletonBoneIndex);
int32 ParentOtherBoneIndex = InRefSkeleton.GetParentIndex(OtherBoneIndex);
// if one of the parents doesn't exist, make sure both end. Otherwise fail.
if( (ParentSkeletonBoneIndex == INDEX_NONE) || (ParentOtherBoneIndex == INDEX_NONE) )
{
return (ParentSkeletonBoneIndex == ParentOtherBoneIndex);
}
// If parents are not named the same, fail.
if( ReferenceSkeleton.GetBoneName(ParentSkeletonBoneIndex) != InRefSkeleton.GetBoneName(ParentOtherBoneIndex) )
{
UE_LOG(LogAnimation, Warning, TEXT("%s : Hierarchy does not match %s - %s."), *ReferenceSkeleton.GetBoneName(SkeletonBoneIndex).ToString(), *ReferenceSkeleton.GetBoneName(ParentSkeletonBoneIndex).ToString(), *InRefSkeleton.GetBoneName(ParentOtherBoneIndex).ToString());
return false;
}
// move up
SkeletonBoneIndex = ParentSkeletonBoneIndex;
OtherBoneIndex = ParentOtherBoneIndex;
} while ( true );
}
bool IsCompatibleReferenceSkeleton(const FReferenceSkeleton& InRefSkeleton, bool bDoParentChainCheck=true) const
{
// at least % of bone should match
int32 NumOfBoneMatches = 0;
const int32 OtherNumBones = InRefSkeleton.GetRawBoneNum();
// first ensure the parent exists for each bone
for (int32 OtherBoneIndex=0; OtherBoneIndex<OtherNumBones; OtherBoneIndex++)
{
const FName OtherBoneName = InRefSkeleton.GetBoneName(OtherBoneIndex);
// See if Mesh bone exists in Skeleton.
int32 SkeletonBoneIndex = ReferenceSkeleton.FindBoneIndex( OtherBoneName );
// if found, increase num of bone matches count
if( SkeletonBoneIndex != INDEX_NONE )
{
++NumOfBoneMatches;
// follow the parent chain to verify the chain is same
if(bDoParentChainCheck && !DoesParentChainMatch(SkeletonBoneIndex, InRefSkeleton))
{
UE_LOG(LogAnimation, Warning, TEXT("%s : Hierarchy does not match."), *OtherBoneName.ToString());
return false;
}
}
else
{
int32 CurrentBoneId = OtherBoneIndex;
// if not look for parents that matches
while (SkeletonBoneIndex == INDEX_NONE && CurrentBoneId != INDEX_NONE)
{
// find Parent one see exists
const int32 ParentOtherBoneIndex = InRefSkeleton.GetParentIndex(CurrentBoneId);
if ( ParentOtherBoneIndex != INDEX_NONE )
{
// @TODO: make sure RefSkeleton's root ParentIndex < 0 if not, I'll need to fix this by checking TreeBoneIdx
FName ParentBoneName = InRefSkeleton.GetBoneName(ParentOtherBoneIndex);
SkeletonBoneIndex = ReferenceSkeleton.FindBoneIndex(ParentBoneName);
}
// root is reached
if( ParentOtherBoneIndex == 0 )
{
break;
}
else
{
CurrentBoneId = ParentOtherBoneIndex;
}
}
// still no match, return false, no parent to look for
if( SkeletonBoneIndex == INDEX_NONE )
{
UE_LOG(LogAnimation, Warning, TEXT("%s : Missing joint on skeleton. Make sure to assign to the skeleton."), *OtherBoneName.ToString());
return false;
}
// second follow the parent chain to verify the chain is same
if (bDoParentChainCheck && !DoesParentChainMatch(SkeletonBoneIndex, InRefSkeleton))
{
UE_LOG(LogAnimation, Warning, TEXT("%s : Hierarchy does not match."), *OtherBoneName.ToString());
return false;
}
}
}
// originally we made sure at least matches more than 50%
// but then follower components can't play since they're only partial
// if the hierarchy matches, and if it's more then 1 bone, we allow
return (NumOfBoneMatches > 0);
}
private:
const FReferenceSkeleton& ReferenceSkeleton;
};
}
FTransform FMirrorOptions::MirrorTransform(const FTransform& InTransform) const
{
FTransform Transform = InTransform;
Transform.SetLocation(MirrorVector(Transform.GetLocation()));
if (bMirrorRotation)
{
FRotator Rotator(ForceInitToZero);
switch (MirrorAxis)
{
case EAxis::X:
{
Rotator.Roll = 180;
break;
}
case EAxis::Y:
{
Rotator.Pitch = 180;
break;
}
case EAxis::Z:
{
Rotator.Yaw = 180;
break;
}
default:
break;
}
Transform.SetRotation(FQuat::MakeFromRotator(Rotator) * Transform.GetRotation());
}
return Transform;
}
FVector FMirrorOptions::MirrorVector(const FVector& InVector) const
{
FVector Axis(ForceInitToZero); Axis.SetComponentForAxis(MirrorAxis, 1.f);
return InVector.MirrorByVector(Axis);
}
FTransform FOrientOptions::OrientTransform(const FVector& InPrimaryTarget, const FTransform& InTransform) const
{
if (Primary == EOrientAxis::None || InPrimaryTarget.IsNearlyZero())
{
return InTransform;
}
auto GetOrientVector = [](const EOrientAxis& InOrientAxis)
{
switch(InOrientAxis)
{
case EOrientAxis::None: return FVector::ZeroVector;
case EOrientAxis::PositiveX: return FVector::XAxisVector;
case EOrientAxis::PositiveY: return FVector::YAxisVector;
case EOrientAxis::PositiveZ: return FVector::ZAxisVector;
case EOrientAxis::NegativeX: return -FVector::XAxisVector;
case EOrientAxis::NegativeY: return -FVector::YAxisVector;
case EOrientAxis::NegativeZ: return -FVector::ZAxisVector;
default: break;
}
return FVector::ZeroVector;
};
FTransform Transform = InTransform;
const FVector PrimaryOrientVector = GetOrientVector(Primary);
const FVector PrimaryAxis = Transform.TransformVectorNoScale(PrimaryOrientVector).GetSafeNormal();
const FVector PrimaryTarget = InPrimaryTarget.GetSafeNormal();
// orient primary axis towards InPrimaryTarget
{
const FQuat Rotation = FQuat::FindBetweenNormals(PrimaryAxis, PrimaryTarget);
const FQuat NewRotation = (Rotation * Transform.GetRotation()).GetNormalized();
Transform.SetRotation(NewRotation);
}
// no need to use secondary axis
if (Secondary == Primary || Secondary == EOrientAxis::None || SecondaryTarget.IsNearlyZero())
{
return Transform;
}
FVector SecondTarget = SecondaryTarget.GetSafeNormal();
if (FMath::IsNearlyEqual(FMath::Abs(FVector::DotProduct(PrimaryTarget, SecondTarget)), 1.0f))
{
// both targets are parallel
return Transform;
}
// orient secondary axis towards SecondaryDirection
{
// project on primary
SecondTarget = SecondTarget - FVector::DotProduct(SecondTarget, PrimaryTarget) * PrimaryTarget;
if (!SecondTarget.IsNearlyZero())
{
SecondTarget = SecondTarget.GetSafeNormal();
const FVector SecondaryOrientVector = GetOrientVector(Secondary);
const FVector SecondaryAxis = Transform.TransformVectorNoScale(SecondaryOrientVector).GetSafeNormal();
// if they are opposites, we only need to rotate 180 degrees around PrimaryTarget
const double DotProduct = FVector::DotProduct(SecondaryAxis, SecondTarget);
const bool bAreOpposites = (DotProduct + 1.0) < KINDA_SMALL_NUMBER;
const FQuat Rotation = bAreOpposites ? FQuat(PrimaryTarget, PI) : FQuat::FindBetweenNormals(SecondaryAxis, SecondTarget);
const FQuat NewRotation = (Rotation * Transform.GetRotation()).GetNormalized();
Transform.SetRotation(NewRotation);
}
}
return Transform;
}
void USkeletonModifier::ExternalUpdate(const FReferenceSkeleton& InRefSkeleton, const TArray<int32>& InIndexTracker)
{
if (!ReferenceSkeleton)
{
return;
}
*ReferenceSkeleton = InRefSkeleton;
TransformComposer.Reset(new FTransformComposer(*ReferenceSkeleton));
BoneIndexTracker = InIndexTracker;
}
bool USkeletonModifier::SetSkeletalMesh(USkeletalMesh* InSkeletalMesh)
{
SkeletalMesh = nullptr;
MeshDescription.Reset();
ReferenceSkeleton.Reset();
TransformComposer.Reset();
BoneIndexTracker.Reset();
#if WITH_EDITORONLY_DATA
// validate supplied skeletal mesh exists
if (!InSkeletalMesh)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: No skeletal mesh supplied to load."));
return false;
}
const USkeleton* Skeleton = InSkeletalMesh->GetSkeleton();
if (!Skeleton)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: Skeletal Mesh supplied has no skeleton."));
return false;
}
// verify user is not trying to modify one of the core engine assets
if (InSkeletalMesh->GetPathName().StartsWith(TEXT("/Engine/")))
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: Cannot modify built-in engine asset."));
return false;
}
// store pointer to mesh and instantiate a mesh description for commiting changes
SkeletalMesh = InSkeletalMesh;
// store mesh description to edit
MeshDescription = MakeUnique<FMeshDescription>();
SkeletalMesh->CloneMeshDescription(USkeletonModifierLocals::LODIndex, *MeshDescription);
if (MeshDescription->IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: mesh description is emtpy."));
return false;
}
// store reference skeleton to edit
ReferenceSkeleton = MakeUnique<FReferenceSkeleton>();
*ReferenceSkeleton = SkeletalMesh->GetRefSkeleton();
TransformComposer.Reset(new FTransformComposer(*ReferenceSkeleton));
// store initial bones indices to track for changes
const int32 NumBones = ReferenceSkeleton->GetRawBoneNum();
BoneIndexTracker.Reserve(NumBones);
for (int32 Index = 0; Index < NumBones; Index++)
{
BoneIndexTracker.Add(Index);
}
return true;
#else
ensureMsgf(false, TEXT("Skeleton Modifier is an editor only feature."));
return false;
#endif
}
bool USkeletonModifier::IsReferenceSkeletonValid(const bool bLog) const
{
if (!ReferenceSkeleton.IsValid())
{
if(bLog)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: No valid reference skeleton provided."));
}
return false;
}
return true;
}
ESkeletalMeshModificationType USkeletonModifier::PreCommitSkeletalMesh()
{
// before commiting, we have to reparent non-root bones with no parent as the animation pipeline
// doesn't support multi-roots
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
if (!BoneInfos.IsEmpty())
{
TArray<FName> BonesToParent;
for (int32 Index = 1; Index < BoneInfos.Num(); Index++)
{
const FMeshBoneInfo& BoneInfo = BoneInfos[Index];
if (BoneInfo.ParentIndex == INDEX_NONE)
{
BonesToParent.Add(BoneInfo.Name);
}
}
if (!BonesToParent.IsEmpty())
{
for (const FName& BoneName: BonesToParent)
{
UE_LOG(LogAnimation, Warning, TEXT("Skeleton Modifier: %s will be parented to the root bone before commiting."), *BoneName.ToString());
}
ParentBones(BonesToParent, {BoneInfos[0].Name});
}
}
// check topological change from this modifier
auto HasBoneIndexesChanged = [this]()
{
for (int32 Index = 0; Index < BoneIndexTracker.Num(); Index++)
{
if (BoneIndexTracker[Index] != Index)
{
return true;
}
}
return false;
};
auto BasicModificationCheck = [this, HasBoneIndexesChanged](const FReferenceSkeleton& InRefSkeleton)
{
const TArray<FMeshBoneInfo>& OtherBoneInfos = InRefSkeleton.GetRawRefBoneInfo();
const TArray<FTransform>& OtherBonePoses = InRefSkeleton.GetRawRefBonePose();
const int32 NumOtherBones = OtherBoneInfos.Num();
const TArray<FMeshBoneInfo>& NewBoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
const TArray<FTransform>& NewBonePoses = ReferenceSkeleton->GetRawRefBonePose();
const int32 NumNewBones = NewBoneInfos.Num();
ESkeletalMeshModificationType Modifications = ESkeletalMeshModificationType::None;
if (NumNewBones > NumOtherBones)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::BonesAdded);
if (HasBoneIndexesChanged())
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::HierarchyChanged);
}
}
else if (NumNewBones < NumOtherBones)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::BonesRemoved);
for (int32 NewBoneIndex = 0; NewBoneIndex < NumNewBones; NewBoneIndex++)
{
// check names
const FName NewBoneName = ReferenceSkeleton->GetBoneName(NewBoneIndex);
const int32 OtherBoneIndex = InRefSkeleton.FindBoneIndex(NewBoneName);
if (OtherBoneIndex == INDEX_NONE)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::BonesRenamed);
}
else
{
// check parents names
const FMeshBoneInfo& NewBoneInfo = NewBoneInfos[NewBoneIndex];
const FMeshBoneInfo& OtherBoneInfo = OtherBoneInfos[OtherBoneIndex];
const int32 NewParentIndex = NewBoneInfo.ParentIndex;
const int32 OldParentIndex = OtherBoneInfo.ParentIndex;
const FName NewParentName = NewParentIndex != INDEX_NONE ? NewBoneInfos[NewParentIndex].Name : NAME_None;
const FName OldParentName = OldParentIndex != INDEX_NONE ? OtherBoneInfos[OldParentIndex].Name : NAME_None;
if ((NewParentIndex != INDEX_NONE || OldParentIndex != INDEX_NONE) && NewParentName != OldParentName)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::HierarchyChanged);
}
}
}
}
else
{
for (int32 NewBoneIndex = 0; NewBoneIndex < NumNewBones; NewBoneIndex++)
{
// check names
const FName NewBoneName = ReferenceSkeleton->GetBoneName(NewBoneIndex);
const int32 OtherBoneIndex = InRefSkeleton.FindBoneIndex(NewBoneName);
if (OtherBoneIndex == INDEX_NONE)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::BonesRenamed);
}
else
{
// check index
if (OtherBoneIndex != NewBoneIndex)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::HierarchyChanged);
}
// check parents
const FMeshBoneInfo& NewBoneInfo = NewBoneInfos[NewBoneIndex];
const FMeshBoneInfo& OtherBoneInfo = OtherBoneInfos[OtherBoneIndex];
if (NewBoneInfo.ParentIndex != OtherBoneInfo.ParentIndex)
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::HierarchyChanged);
}
// check transforms
const FTransform& NewBoneTransform = NewBonePoses[NewBoneIndex];
const FTransform& OtherBoneTransform = OtherBonePoses[OtherBoneIndex];
if (!NewBoneTransform.Equals(OtherBoneTransform))
{
EnumAddFlags(Modifications, ESkeletalMeshModificationType::TransformChanged);
}
}
}
}
return Modifications;
};
return BasicModificationCheck(SkeletalMesh->GetRefSkeleton());
}
ESkeletonModificationType USkeletonModifier::PreCommitSkeleton(const ESkeletalMeshModificationType InSkeletalMeshModifications) const
{
const bool bNeedSkeletonUpdate = EnumHasAnyFlags(InSkeletalMeshModifications, ESkeletalMeshModificationType::SkeletonUpdated);
if (!bNeedSkeletonUpdate)
{
return ESkeletonModificationType::None;
}
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
USkeletonModifierLocals::FReferenceSkeletonCompatibilityChecker Checker(Skeleton->GetReferenceSkeleton());
if (Checker.IsCompatibleReferenceSkeleton(*ReferenceSkeleton))
{
// skeleton is compatible
return ESkeletonModificationType::SimpleMerge;
}
USkeletalMeshMergeOptions* Options = NewObject<USkeletalMeshMergeOptions>();
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
TSharedRef<IDetailsView> DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
static constexpr bool bForceRefresh = true;
DetailsView->SetObject(Options, bForceRefresh);
TSharedRef<SCustomDialog> OptionsDialog = SNew(SCustomDialog)
.Title(NSLOCTEXT("SkeletonModifier", "SkeletonModifierMergeDialog", "SkeletonModifier Merge Options"))
.Content()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.Padding(4.f, 2.f)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SkeletonModifier", "SkeletonModifierMergeText",
"The current changes to the edited bone hierarchy are incompatible with the assigned skeleton asset.\n"
"'Commit' to commit the current changes using the merge type below.\n"
"'Cancel' to cancel the current changes and revert to the previous skeleton.\n"))
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(4.f, 2.f)
[
SNew(SBox)
.MinDesiredWidth(450)
[
DetailsView
]
]
]
.Buttons({
SCustomDialog::FButton(NSLOCTEXT("SkeletonModifier", "CommitButton", "Commit")),
SCustomDialog::FButton(NSLOCTEXT("SkeletonModifier", "CancelButton", "Cancel"))
});
const int32 Choice = OptionsDialog->ShowModal();
if (Choice == 1 || Choice == -1)
{
return ESkeletonModificationType::Cancel;
}
switch (Options->MergeType)
{
case ESKeletalMeshMergeType::New:
return ESkeletonModificationType::DuplicateAndMerge;
case ESKeletalMeshMergeType::Merge:
return Options->bMergeAll ? ESkeletonModificationType::FullMergeAll : ESkeletonModificationType::FullMerge;
default:
break;
}
return ESkeletonModificationType::None;
}
bool USkeletonModifier::CommitSkeletonToSkeletalMesh()
{
if (!SkeletalMesh.IsValid() || !ReferenceSkeleton || !MeshDescription)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier: No mesh loaded. Cannot apply skeleton edits."));
return false;
}
#if WITH_EDITORONLY_DATA
// check modifications at the skeletal mesh level
const ESkeletalMeshModificationType Modifications = PreCommitSkeletalMesh();
if (Modifications == ESkeletalMeshModificationType::None)
{
UE_LOG(LogAnimation, Warning, TEXT("Skeleton Modifier: No modification needed."));
return false;
}
// check modifications at the skeleton level
ESkeletonModificationType SkeletonModifications = PreCommitSkeleton(Modifications);
if (SkeletonModifications == ESkeletonModificationType::Cancel)
{
UE_LOG(LogAnimation, Warning, TEXT("Skeleton Modifier: Skeleton can't be modified."));
return false;
}
// duplicate the skeleton if needed
if (SkeletonModifications == ESkeletonModificationType::DuplicateAndMerge)
{
TArray<UObject*> SavedObjects;
FEditorFileUtils::SaveAssetsAs({SkeletalMesh->GetSkeleton()}, SavedObjects);
USkeleton* NewSkeleton = SavedObjects.IsEmpty() ? nullptr : Cast<USkeleton>(SavedObjects[0]);
if (!NewSkeleton)
{
UE_LOG(LogAnimation, Warning, TEXT("Skeleton Modifier: Skeleton can't be duplicated."));
return false;
}
SkeletalMesh->SetSkeleton(NewSkeleton);
SkeletalMesh->MarkPackageDirty();
if (NewSkeleton->GetPreviewMesh() != SkeletalMesh)
{
NewSkeleton->SetPreviewMesh(SkeletalMesh.Get());
}
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
// update mesh description
CommitChangesToMeshDescription(Modifications);
// store retargeting modes
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
TArray<EBoneTranslationRetargetingMode::Type> RetargetingModes;
RetargetingModes.Init(EBoneTranslationRetargetingMode::Animation, BoneInfos.Num());
for (int32 OldBoneIndex = 0, NumOldBones = BoneIndexTracker.Num(); OldBoneIndex < NumOldBones; ++OldBoneIndex)
{
const int32 NewBoneIndex = BoneIndexTracker[OldBoneIndex];
if (RetargetingModes.IsValidIndex(NewBoneIndex))
{
RetargetingModes[NewBoneIndex] = Skeleton->GetBoneTranslationRetargetingMode(OldBoneIndex);
}
}
// update skeletal mesh
FlushRenderingCommands();
// call modify on the skeleton first as post undo will re-register components so it must be done once both
// skeletal mesh and skeleton are up to date, so it must be done after the skeletal mesh has been undone
if (EnumHasAnyFlags(SkeletonModifications, ESkeletonModificationType::DoUpdate))
{
Skeleton->Modify();
}
SkeletalMesh->SetFlags(RF_Transactional);
SkeletalMesh->Modify();
// update the ref skeleton
SkeletalMesh->SetRefSkeleton(*ReferenceSkeleton);
SkeletalMesh->GetRefBasesInvMatrix().Reset();
SkeletalMesh->CalculateInvRefMatrices();
// update skeletal mesh LOD (cf. USkeletalMesh::CommitMeshDescription)
SkeletalMesh->ModifyMeshDescription(USkeletonModifierLocals::LODIndex);
SkeletalMesh->CreateMeshDescription(USkeletonModifierLocals::LODIndex, MoveTemp(*MeshDescription));
SkeletalMesh->CommitMeshDescription(USkeletonModifierLocals::LODIndex);
// update skeleton
if (EnumHasAnyFlags(SkeletonModifications, ESkeletonModificationType::DoUpdate))
{
NotifyFromSkeletonChanges();
auto UpdateSkeleton = [this, &SkeletonModifications, &RetargetingModes]()
{
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
if (SkeletonModifications == ESkeletonModificationType::SimpleMerge)
{
constexpr bool bSkeletalMeshReferencesOnly = false;
if (!HasAnyOtherReferences(bSkeletalMeshReferencesOnly))
{
return Skeleton->MergeAllBonesToBoneTree(SkeletalMesh.Get());
}
SkeletonModifications = ESkeletonModificationType::FullMerge;
}
bool bSkeletonModified = false;
if (EnumHasAnyFlags(SkeletonModifications, ESkeletonModificationType::DeepMerge))
{
bSkeletonModified = Skeleton->RecreateBoneTree(SkeletalMesh.Get());
}
if (!bSkeletonModified)
{
return false;
}
// restore retargeting modes
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
for (int32 BoneIndex = 0, NumBones = BoneInfos.Num(); BoneIndex < NumBones; ++BoneIndex)
{
Skeleton->SetBoneTranslationRetargetingMode(BoneIndex, RetargetingModes[BoneIndex]);
}
return true;
};
if (UpdateSkeleton())
{
PostCommitSkeleton(SkeletonModifications);
Skeleton->MarkPackageDirty();
FAssetNotifications::SkeletonNeedsToBeSaved(Skeleton);
}
else
{
ensure(false);
}
}
// must be done once the skeleton is up to date
SkeletalMesh->PostEditChange();
return true;
#else
ensureMsgf(false, TEXT("Skeleton Modifier is an editor only feature."));
return false;
#endif
}
void USkeletonModifier::CommitChangesToMeshDescription(const ESkeletalMeshModificationType InSkeletalMeshModifications)
{
if (!SkeletalMesh.IsValid() || !ReferenceSkeleton || !MeshDescription)
{
// this is supposed to be tested earlier
return;
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
const TArray<FTransform>& Transforms = ReferenceSkeleton->GetRawRefBonePose();
FSkeletalMeshAttributes MeshAttributes(*MeshDescription);
// update bone data
if (!MeshAttributes.HasBones())
{
MeshAttributes.Register(true);
}
MeshAttributes.Bones().Reset(BoneInfos.Num());
FSkeletalMeshAttributes::FBoneNameAttributesRef BoneNames = MeshAttributes.GetBoneNames();
FSkeletalMeshAttributes::FBoneParentIndexAttributesRef BoneParentIndices = MeshAttributes.GetBoneParentIndices();
FSkeletalMeshAttributes::FBonePoseAttributesRef BonePoses = MeshAttributes.GetBonePoses();
for (int Index = 0; Index < BoneInfos.Num(); ++Index)
{
const FMeshBoneInfo& Info = BoneInfos[Index];
const FBoneID BoneID = MeshAttributes.CreateBone();
BoneNames.Set(BoneID, Info.Name);
BoneParentIndices.Set(BoneID, Info.ParentIndex);
BonePoses.Set(BoneID, Transforms[Index]);
}
// update skin weight data if needed
if (EnumHasAnyFlags(InSkeletalMeshModifications, ESkeletalMeshModificationType::IndicesUpdated))
{
using namespace UE::AnimationCore;
FBoneWeightsSettings BoneSettings; BoneSettings.SetNormalizeType(EBoneWeightNormalizeType::None);
for (const FName SkinWeightProfile: MeshAttributes.GetSkinWeightProfileNames())
{
FSkinWeightsVertexAttributesRef SkinWeights = MeshAttributes.GetVertexSkinWeights(SkinWeightProfile);
if (SkinWeights.IsValid())
{
for (const FVertexID& VertexID: MeshDescription->Vertices().GetElementIDs())
{
FVertexBoneWeights BoneWeights = SkinWeights.Get(VertexID);
if (const int32 NumBoneWeights = BoneWeights.Num())
{
TArray<FBoneWeight> NewWeights;
for (int32 Idx = 0; Idx < NumBoneWeights; ++Idx)
{
const FBoneWeight& OldBoneWeight = BoneWeights[Idx];
const int32 BoneIndex = OldBoneWeight.GetBoneIndex();
int32 NewBoneIndex = 0;
if (ensure(BoneIndexTracker.IsValidIndex(BoneIndex)))
{
NewBoneIndex = BoneIndexTracker[BoneIndex];
}
else
{
UE_LOG(LogAnimation, Warning, TEXT("Skeleton Modifier - Commit: Invalid bone index provided (%d); falling back to 0 as bone index."), BoneIndex);
}
if (NewBoneIndex != INDEX_NONE)
{
NewWeights.Add(FBoneWeight(NewBoneIndex, OldBoneWeight.GetRawWeight()));
}
}
SkinWeights.Set(VertexID, FBoneWeights::Create(NewWeights, BoneSettings));
}
}
}
}
}
}
void USkeletonModifier::PostCommitSkeleton(const ESkeletonModificationType InSkeletonModifications) const
{
if (InSkeletonModifications != ESkeletonModificationType::FullMergeAll)
{
return;
}
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
TArray<const USkeletalMesh*> OtherSkeletalMeshUsingSkeleton;
const IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
FARFilter ARFilter;
ARFilter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
ARFilter.TagsAndValues.Add(TEXT("Skeleton"), FAssetData(Skeleton).GetExportTextName());
TArray<FAssetData> SkeletalMeshAssetData;
if (AssetRegistry.GetAssets(ARFilter, SkeletalMeshAssetData))
{
for (const FAssetData& AssetData: SkeletalMeshAssetData)
{
const USkeletalMesh* ExtraSkeletalMesh = Cast<USkeletalMesh>(AssetData.GetAsset());
if (IsValid(ExtraSkeletalMesh) && ExtraSkeletalMesh != SkeletalMesh)
{
OtherSkeletalMeshUsingSkeleton.Add(ExtraSkeletalMesh);
}
}
}
for (const USkeletalMesh* ExtraSkeletalMesh : OtherSkeletalMeshUsingSkeleton)
{
// merge still can fail
if (!Skeleton->MergeAllBonesToBoneTree(ExtraSkeletalMesh))
{
FMessageDialog::Open(EAppMsgType::Ok,
FText::Format(NSLOCTEXT("SkeletonModifier", "SkeletonModifier_RemergingBones", "Failed to merge SkeletalMesh '{0}'."), FText::FromString(ExtraSkeletalMesh->GetName())));
}
}
}
bool USkeletonModifier::HasAnyOtherReferences(const bool bSkeletalMeshOnly) const
{
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
UPackage* SkeletonPackage = Skeleton->GetPackage();
UPackage* SkeletalMeshPackage = SkeletalMesh->GetPackage();
if (SkeletonPackage && SkeletalMeshPackage)
{
TArray<FSoftObjectPath> Result;
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.GetRegistry();
TArray<FName> References;
AssetRegistry.GetReferencers(SkeletonPackage->GetFName(), References);
const FName SkeletalMeshPackageName = SkeletalMeshPackage->GetFName();
TArray<FName> Dependencies;
for (const FName& Reference : References)
{
if (SkeletalMeshPackageName != Reference)
{
const FString PackageString = Reference.ToString();
const FSoftObjectPath FullAssetPath(FTopLevelAssetPath(*PackageString, *FPackageName::GetLongPackageAssetName(PackageString)));
FAssetData AssetData = AssetRegistry.GetAssetByObjectPath(FullAssetPath);
if (UClass* Class = AssetData.GetClass())
{
if (!bSkeletalMeshOnly || Class == USkeletalMesh::StaticClass())
{
Dependencies.AddUnique(*AssetData.GetObjectPathString());
}
}
}
}
return !Dependencies.IsEmpty();
}
return false;
}
void USkeletonModifier::NotifyFromSkeletonChanges() const
{
// check assets using this skeleton ?
if (bDebug)
{
// TODO avoid certain changes when the skeleton is referenced by other assets (i.e. changing the skeletal mesh's
// reference skeleton poses is fine, re-parenting/removing bones, etc. is not)
static const TArray<FTopLevelAssetPath> AssetPaths({
UAnimSequence::StaticClass()->GetClassPathName(),
UAnimMontage::StaticClass()->GetClassPathName(),
UPoseAsset::StaticClass()->GetClassPathName(),
USkeletalMesh::StaticClass()->GetClassPathName()});
FARFilter Filter;
Filter.ClassPaths = AssetPaths;
const IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
TArray<FAssetData> Assets; AssetRegistry.GetAssets(Filter, Assets);
const FAssetData SkeletonAssetData(SkeletalMesh->GetSkeleton());
const FString SkeletonPath = SkeletonAssetData.GetExportTextName();
static const FName Tag("Skeleton");
for (const FAssetData& AssetData: Assets)
{
const FString TagValue = AssetData.GetTagValueRef<FString>(Tag);
if (TagValue == SkeletonPath)
{
UE_LOG(LogAnimation, Warning, TEXT("%s references that skeleton."), *AssetData.GetExportTextName());
}
}
}
}
bool USkeletonModifier::AddBone(const FName InBoneName, const FName InParentName, const FTransform& InTransform)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Add: Cannot add bone with no name."));
return false;
}
return AddBones({InBoneName}, {InParentName}, {InTransform});
}
bool USkeletonModifier::AddBones(
const TArray<FName>& InBoneNames, const TArray<FName>& InParentNames, const TArray<FTransform>& InTransforms)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
const int32 NumBonesToAdd = InBoneNames.Num();
if (NumBonesToAdd == 0)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Add: The provided bone names array is empty."));
return false;
}
struct FBoneData
{
FMeshBoneInfo BoneInfo;
int32 TransformOffset;
};
TArray<FBoneData> BonesToAdd;
BonesToAdd.Reserve(NumBonesToAdd);
auto GetParentName = [&](int32 Index)
{
if (InBoneNames.Num() == InParentNames.Num())
{
return InParentNames[Index];
}
return InParentNames.IsEmpty() ? NAME_None : InParentNames[0];
};
const int NumBonesBefore = ReferenceSkeleton->GetRawBoneNum();
for (int32 Index = 0; Index < NumBonesToAdd; Index++)
{
const FName& BoneName = InBoneNames[Index];
if (ReferenceSkeleton->FindBoneIndex(InBoneNames[Index]) == INDEX_NONE)
{
const FName ParentName = GetParentName(Index);
// look for parent index in the ref skeleton
int32 ParentIndex = ReferenceSkeleton->FindBoneIndex(ParentName);
if (ParentIndex == INDEX_NONE && Index > 0)
{
// otherwise, check if one of the new bone is going to be the parent
ParentIndex = InBoneNames.IndexOfByKey(ParentName);
if (ParentIndex > INDEX_NONE && ParentIndex < Index)
{
ParentIndex += NumBonesBefore;
}
}
const FMeshBoneInfo NewBoneInfo(BoneName, BoneName.ToString(), ParentIndex);
BonesToAdd.Add({NewBoneInfo, Index});
}
}
if (BonesToAdd.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Add: None of the provided names is avalable to be added."));
return false;
}
auto GetTransform = [&](int32 Index)
{
if (InBoneNames.Num() == InTransforms.Num())
{
return InTransforms[Index];
}
return InTransforms.IsEmpty() ? FTransform::Identity : InTransforms[0];
};
//update reference skeleton
{
static constexpr bool bAllowMultipleRoots = true;
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
for (FBoneData& BoneData: BonesToAdd)
{
Modifier.Add(BoneData.BoneInfo, GetTransform(BoneData.TransformOffset), bAllowMultipleRoots);
}
}
// invalidate composer
TransformComposer->Invalidate(INDEX_NONE);
// update index tracker: nothing to do as those new indices do not represent any bone in the initial skinning data
return true;
}
bool USkeletonModifier::MirrorBone(const FName InBoneName, const FMirrorOptions& InOptions)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Mirror: Cannot mirror bone with no name."));
return false;
}
return MirrorBones({InBoneName}, InOptions);
}
bool USkeletonModifier::MirrorBones(const TArray<FName>& InBonesName, const FMirrorOptions& InOptions)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
// get bones to mirror
TArray<int32> BonesToMirror; GetBonesToMirror(InBonesName, InOptions, BonesToMirror);
const int32 NumBonesToMirror = BonesToMirror.Num();
if (NumBonesToMirror == 0)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Mirror: None of the provided names has been found."));
return false;
}
// get mirrored names
TArray<FName> MirroredNames; GetMirroredNames(BonesToMirror, InOptions, MirroredNames);
// add bones first if they are missing
TArray<int32> MirroredBones; GetMirroredBones(BonesToMirror, MirroredNames, MirroredBones);
if (MirroredBones.Num() != NumBonesToMirror)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Mirror: Couldn't find mirrored bones."));
return false;
}
// compute mirrored transforms
TArray<FTransform> MirroredTransforms; GetMirroredTransforms(BonesToMirror, MirroredBones, InOptions, MirroredTransforms);
// update reference skeleton
{
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
for (int32 Index = 0; Index < NumBonesToMirror; Index++)
{
Modifier.UpdateRefPoseTransform(MirroredBones[Index], MirroredTransforms[Index]);
}
}
// invalidate composer
TransformComposer->Invalidate(INDEX_NONE);
return true;
}
void USkeletonModifier::GetBonesToMirror(
const TArray<FName>& InBonesName, const FMirrorOptions& InOptions, TArray<int32>& OutBonesToMirror) const
{
OutBonesToMirror.Reset();
TSet<int32> IndicesToMirror;
auto GetBonesToMirror = [&](const int32 BoneIndex, auto&& GetBonesToMirror2) -> void
{
if (BoneIndex == INDEX_NONE)
{
return;
}
IndicesToMirror.Add(BoneIndex);
if (InOptions.bMirrorChildren)
{
TArray<int32> Children; ReferenceSkeleton->GetRawDirectChildBones(BoneIndex, Children);
for (int32 ChildIndex: Children)
{
GetBonesToMirror2(ChildIndex, GetBonesToMirror2);
}
}
};
for (const FName& BoneName: InBonesName)
{
GetBonesToMirror(ReferenceSkeleton->FindRawBoneIndex(BoneName), GetBonesToMirror);
}
const int32 NumBonesToMirror = IndicesToMirror.Num();
if (NumBonesToMirror == 0)
{
return;
}
IndicesToMirror.Sort([](const int32 Index0, const int32 Index1) {return Index0 < Index1;});
OutBonesToMirror = IndicesToMirror.Array();
}
void USkeletonModifier::GetMirroredNames(
const TArray<int32>& InBonesToMirror, const FMirrorOptions& InOptions, TArray<FName>& OutBonesName) const
{
OutBonesName.Reset();
if (InBonesToMirror.IsEmpty())
{
return;
}
const FName Left(InOptions.LeftString), Right(InOptions.RightString);
const TArray MirrorFindReplaceExpressions({
FMirrorFindReplaceExpression(Left, Right, EMirrorFindReplaceMethod::Suffix),
FMirrorFindReplaceExpression(Right, Left, EMirrorFindReplaceMethod::Suffix)
});
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
OutBonesName.Reserve(InBonesToMirror.Num());
Algo::Transform(InBonesToMirror, OutBonesName, [&](const int32 BoneIndex)
{
const FName BoneName = BoneInfos[BoneIndex].Name;
const FName MirrorName = UMirrorDataTable::GetMirrorName(BoneName, MirrorFindReplaceExpressions);
if (MirrorName.IsNone() || MirrorName == BoneName)
{
return GetUniqueName(BoneName, OutBonesName);
}
return MirrorName;
});
}
void USkeletonModifier::GetMirroredBones(
const TArray<int32>& InBonesToMirror, const TArray<FName>& InMirroredNames, TArray<int32>& OutMirroredBones)
{
const int32 NumBones = InBonesToMirror.Num();
OutMirroredBones.Reset();
if (InBonesToMirror.IsEmpty() || NumBones != InMirroredNames.Num())
{
return;
}
// check mirrored names uniqueness
const TSet<FName> UniqueMirroredNames(InMirroredNames);
if (UniqueMirroredNames.Num() != NumBones)
{
return;
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
const TArray<FTransform>& BoneTransforms = ReferenceSkeleton->GetRawRefBonePose();
TArray<FName> BonesToAdd, ParentNames;
TArray<FTransform> Transforms;
for (int32 Index = 0; Index < NumBones; Index++)
{
const FName& MirroredName = InMirroredNames[Index];
const int32 MirroredIndex = ReferenceSkeleton->FindRawBoneIndex(MirroredName);
if (MirroredIndex == INDEX_NONE)
{
const int32 RefBoneIndex = InBonesToMirror[Index];
// name
BonesToAdd.Add(MirroredName);
// parent
int32 ParentIndex = BoneInfos[RefBoneIndex].ParentIndex;
FName ParentName = NAME_None;
if (ParentIndex != INDEX_NONE)
{
ParentName = BoneInfos[ParentIndex].Name;
// is that parent being mirrored?
const int32 ParentIndexInMirrored = InBonesToMirror.IndexOfByKey(ParentIndex);
if (ParentIndexInMirrored != INDEX_NONE)
{
ParentName = InMirroredNames[ParentIndexInMirrored];
}
}
ParentNames.Add(ParentName);
// transform
Transforms.Add(BoneTransforms[RefBoneIndex]);
}
else
{
OutMirroredBones.Add(MirroredIndex);
}
}
if (BonesToAdd.IsEmpty())
{
return;
}
// add missing bones and get their index
OutMirroredBones.Reset();
if (!AddBones(BonesToAdd, ParentNames, Transforms))
{
return;
}
OutMirroredBones.Reserve(NumBones);
for (int32 Index = 0; Index < NumBones; Index++)
{
const FName& MirroredName = InMirroredNames[Index];
const int32 MirroredIndex = ReferenceSkeleton->FindRawBoneIndex(MirroredName);
if (MirroredIndex == INDEX_NONE)
{
OutMirroredBones.Reset();
return;
}
OutMirroredBones.Add(MirroredIndex);
}
}
void USkeletonModifier::GetMirroredTransforms(
const TArray<int32>& InBonesToMirror, const TArray<int32>& InMirroredBones,
const FMirrorOptions& InOptions, TArray<FTransform>& OutMirroredTransforms) const
{
OutMirroredTransforms.Reset();
const int32 NumBonesToMirror = InBonesToMirror.Num();
if (NumBonesToMirror == 0)
{
return;
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
auto FindFirstNotMirroredParent = [&](const int32 RefBoneIndex) -> int32
{
if (RefBoneIndex == INDEX_NONE)
{
return INDEX_NONE;
}
int32 RefParentIndex = BoneInfos[RefBoneIndex].ParentIndex;
if (RefParentIndex == INDEX_NONE)
{
return INDEX_NONE;
}
int32 ParentMirroredIndex = InBonesToMirror.IndexOfByKey(RefParentIndex);
while (ParentMirroredIndex != INDEX_NONE)
{
RefParentIndex = BoneInfos[RefParentIndex].ParentIndex;
ParentMirroredIndex = InBonesToMirror.IndexOfByKey(RefParentIndex);
}
return RefParentIndex;
};
// compute global mirrored transforms
TArray<FTransform> MirroredGlobal; MirroredGlobal.Reserve(NumBonesToMirror);
for (int32 Index = 0; Index < NumBonesToMirror; Index++)
{
// bone global
const int32 RefBoneIndex = InBonesToMirror[Index];
FTransform Global = TransformComposer->GetGlobalTransform(RefBoneIndex);
// first parent not mirrored global
const int32 FirstNotMirroredParent = FindFirstNotMirroredParent(RefBoneIndex);
const FTransform& FirstNotMirroredGlobal = TransformComposer->GetGlobalTransform(FirstNotMirroredParent);
// switch to first parent not mirrored (translation only)
Global.AddToTranslation(-FirstNotMirroredGlobal.GetTranslation());
// mirror
Global = InOptions.MirrorTransform(Global);
// switch back to global (translation only)
Global.AddToTranslation(FirstNotMirroredGlobal.GetTranslation());
MirroredGlobal.Add(Global);
}
// switch back to local
OutMirroredTransforms.Reserve(NumBonesToMirror);
for (int32 Index = 0; Index < NumBonesToMirror; Index++)
{
const int32 RefBoneIndex = InBonesToMirror[Index];
const int32 RefParentIndex = BoneInfos[RefBoneIndex].ParentIndex;
const int32 ParentMirroredIndex = InBonesToMirror.IndexOfByKey(RefParentIndex);
const int32 ParentIndex = BoneInfos[InMirroredBones[Index]].ParentIndex;
const FTransform& ParentGlobal = ParentMirroredIndex != INDEX_NONE ? MirroredGlobal[ParentMirroredIndex] :
TransformComposer->GetGlobalTransform(ParentIndex);
OutMirroredTransforms.Add(MirroredGlobal[Index].GetRelativeTransform(ParentGlobal));
}
}
// NOTE : that function might take a bUpdateChildren to decide whether we want to compensate the children transforms
// atm, we update the bone's local ref transform so children's global transforms are changed (we just need to cache the
// global transforms then restore them back)
// orienting the bone for example should change the children's global transform
bool USkeletonModifier::SetBoneTransform( const FName InBoneName, const FTransform& InNewTransform, const bool bMoveChildren)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Move: Cannot move bone with no name."));
return false;
}
return SetBonesTransforms({InBoneName}, {InNewTransform}, bMoveChildren);
}
bool USkeletonModifier::SetBonesTransforms(
const TArray<FName>& InBoneNames, const TArray<FTransform>& InNewTransforms, const bool bMoveChildren)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
const int32 NumBonesToMove = InBoneNames.Num();
if (NumBonesToMove == 0 || NumBonesToMove != InNewTransforms.Num())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Move: Discrepancy between bones and transforms (%d / %d)."), NumBonesToMove, InNewTransforms.Num());
return false;
}
TArray<int32> BoneIndices, Offsets;
BoneIndices.Reserve(NumBonesToMove);
Offsets.Reserve(NumBonesToMove);
for (int32 Index = 0; Index < NumBonesToMove; Index++)
{
const int32 BoneIndex = ReferenceSkeleton->FindRawBoneIndex(InBoneNames[Index]);
if (BoneIndex != INDEX_NONE)
{
BoneIndices.Add(BoneIndex);
Offsets.Add(Index);
}
}
if (BoneIndices.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Move: None of the provided bones has been found."));
return false;
}
// compute global transforms if needed
TArray<int32> ChildrenToFix;
TArray<FTransform> GlobalTransforms;
if (!bMoveChildren)
{
// get children
for (int32 Index = 0; Index < BoneIndices.Num(); Index++)
{
TArray<int32> Children;
ReferenceSkeleton->GetRawDirectChildBones(BoneIndices[Index], Children);
for (int32 ChildIndex: Children)
{
if (!BoneIndices.Contains(ChildIndex))
{
ChildrenToFix.Add(ChildIndex);
}
}
}
// sort them from highest index to lowest
ChildrenToFix.Sort([](const int32 Index0, const int32 Index1) {return Index0 > Index1;} );
const int32 NumChildren = ChildrenToFix.Num();
// compute global transforms (note that we could cache them for faster implementation)
GlobalTransforms.AddUninitialized(NumChildren);
for (int32 Index = 0; Index < NumChildren; Index++)
{
GlobalTransforms[Index] = TransformComposer->GetGlobalTransform(ChildrenToFix[Index]);
}
}
// update reference skeleton
{
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
for (int32 Index = 0; Index < BoneIndices.Num(); Index++)
{
FTransform NewTransform = InNewTransforms[Offsets[Index]];
NewTransform.NormalizeRotation();
Modifier.UpdateRefPoseTransform(BoneIndices[Index], NewTransform);
// invalidate cached global transform
TransformComposer->Invalidate(BoneIndices[Index]);
}
if (!bMoveChildren)
{
const int32 NumChildren = ChildrenToFix.Num();
for (int32 Index = 0; Index < NumChildren; Index++)
{
const int32 ChildrenIndex = ChildrenToFix[Index];
const int32 ParentIndex = ReferenceSkeleton->GetRawParentIndex(ChildrenIndex);
const FTransform& NewParentGlobal = TransformComposer->GetGlobalTransform(ParentIndex);
FTransform NewLocal = GlobalTransforms[Index].GetRelativeTransform(NewParentGlobal);
NewLocal.NormalizeRotation();
Modifier.UpdateRefPoseTransform(ChildrenIndex, NewLocal);
TransformComposer->Invalidate(ChildrenIndex);
}
}
}
// update index tracker: no modification on bone indices to track when changing transforms
return true;
}
bool USkeletonModifier::RemoveBone(const FName InBoneName, const bool bRemoveChildren)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Remove: Cannot remove bone with no name."));
return false;
}
return RemoveBones({InBoneName}, bRemoveChildren);
}
bool USkeletonModifier::RemoveBones(const TArray<FName>& InBoneNames, const bool bRemoveChildren)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
if (InBoneNames.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Remove: No bone provided."));
return false;
}
// store initial data
const TArray<FMeshBoneInfo> InfosBeforeRemoval = ReferenceSkeleton->GetRawRefBoneInfo();
TArray<FName> BonesToRemove;
if (bRemoveChildren)
{
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
auto IsParentToBeRemoved = [&](const FName BoneName)
{
const int32 BoneIndex = ReferenceSkeleton->FindRawBoneIndex(BoneName);
if (BoneIndex != INDEX_NONE)
{
int32 ParentIndex = BoneInfos[BoneIndex].ParentIndex;
while (ParentIndex != INDEX_NONE)
{
if (InBoneNames.Contains(BoneInfos[ParentIndex].Name))
{
return true;
}
ParentIndex = BoneInfos[ParentIndex].ParentIndex;
}
}
return false;
};
for (const FName& BoneName: InBoneNames)
{
if (!IsParentToBeRemoved(BoneName))
{
BonesToRemove.Add(BoneName);
}
}
}
else
{
BonesToRemove = InBoneNames;
}
// update reference skeleton
{
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
for (const FName& BoneName: BonesToRemove)
{
Modifier.Remove(BoneName, bRemoveChildren);
}
}
if (InfosBeforeRemoval.Num() == ReferenceSkeleton->GetRawRefBoneInfo().Num())
{ // no bone has been removed
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Remove: No bone has been removed."));
return false;
}
// invalidate composer
TransformComposer->Invalidate(INDEX_NONE);
// update index tracker
UpdateBoneTracker(InfosBeforeRemoval);
return true;
}
bool USkeletonModifier::RenameBone(const FName InOldBoneName, const FName InNewBoneName)
{
if (InOldBoneName == NAME_None || InNewBoneName == NAME_None || InNewBoneName == InOldBoneName)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Rename: cannot rename %s with %s."), *InOldBoneName.ToString(), *InNewBoneName.ToString());
return false;
}
return RenameBones({InOldBoneName}, {InNewBoneName});
}
bool USkeletonModifier::RenameBones(const TArray<FName>& InOldBoneNames, const TArray<FName>& InNewBoneNames)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
if (InOldBoneNames.IsEmpty() || InNewBoneNames.Num() != InOldBoneNames.Num())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Rename: Discrepancy between old and new names (%d / %d)."), InOldBoneNames.Num(), InNewBoneNames.Num());
return false;
}
// update reference skeleton
{
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
TArray<FName> NewBoneNames;
const int32 NumBonesToRename = InOldBoneNames.Num();
for (int32 Index = 0; Index < NumBonesToRename; ++Index)
{
const FName& OldName = InOldBoneNames[Index];
const FName NewName = GetUniqueName(InNewBoneNames[Index], NewBoneNames);
if (OldName != NAME_None && !OldName.IsEqual(NewName, ENameCase::CaseSensitive))
{
NewBoneNames.Add(NewName);
Modifier.Rename(OldName, NewName);
}
}
}
// update index tracker: no modification on bone indices to track when renaming
return true;
}
bool USkeletonModifier::ParentBone(const FName InBoneName, const FName InParentName)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Parent: Cannot parent a bone with no name."));
return false;
}
return ParentBones({InBoneName}, {InParentName});
}
bool USkeletonModifier::ParentBones(const TArray<FName>& InBoneNames, const TArray<FName>& InParentNames)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
if (InBoneNames.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Parent: No bone provided."));
return false;
}
// store initial data
const TArray<FMeshBoneInfo> InfosBeforeParenting = ReferenceSkeleton->GetRawRefBoneInfo();
// update reference skeleton
{
auto GetParentName = [&](int32 Index)
{
if (InBoneNames.Num() == InParentNames.Num())
{
return InParentNames[Index];
}
return InParentNames.IsEmpty() ? NAME_None : InParentNames[0];
};
static constexpr bool bAllowMultipleRoots = true;
FReferenceSkeletonModifier Modifier(*ReferenceSkeleton, nullptr);
for (int32 Index = 0; Index < InBoneNames.Num(); ++Index)
{
const int32 BoneIndex = ReferenceSkeleton->FindRawBoneIndex(InBoneNames[Index]);
if (BoneIndex != INDEX_NONE)
{
const FName NewParentName = GetParentName(Index);
// change parent
const int32 NewIndex = Modifier.SetParent(InBoneNames[Index], NewParentName, bAllowMultipleRoots);
if (NewIndex > INDEX_NONE)
{
// invalidate composer
TransformComposer->Invalidate(INDEX_NONE);
}
}
}
}
// update index tracker
UpdateBoneTracker(InfosBeforeParenting);
return true;
}
bool USkeletonModifier::OrientBone(const FName InBoneName, const FOrientOptions& InOptions)
{
if (InBoneName == NAME_None)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Orient: Cannot orient a bone with no name."));
return false;
}
return OrientBones({InBoneName}, InOptions);
}
bool USkeletonModifier::OrientBones(const TArray<FName>& InBoneNames, const FOrientOptions& InOptions)
{
if (!IsReferenceSkeletonValid())
{
return false;
}
if (InBoneNames.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Orient: No bone provided."));
return false;
}
// get bones to mirror
TArray<int32> BonesToOrient; GetBonesToOrient(InBoneNames, InOptions, BonesToOrient);
const int32 NumBonesToOrient = BonesToOrient.Num();
if (NumBonesToOrient == 0)
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Orient: None of the provided names has been found."));
return false;
}
auto GetAlignedTransform = [&](const int32 BoneIndex) -> FTransform
{
const FTransform& BoneGlobal = TransformComposer->GetGlobalTransform(BoneIndex);
const int32 ParentIndex = BoneIndex != INDEX_NONE ? ReferenceSkeleton->GetRawParentIndex(BoneIndex) : INDEX_NONE;
TArray<int32> Children; ReferenceSkeleton->GetRawDirectChildBones(BoneIndex, Children);
const int32 NumChildren = Children.Num();
if (NumChildren > 1)
{ // we can't align if there are more than one children
return BoneGlobal;
}
const FTransform& ParentGlobal = TransformComposer->GetGlobalTransform(ParentIndex);
FVector Direction = (BoneGlobal.GetLocation() - ParentGlobal.GetLocation()).GetSafeNormal();
if (NumChildren > 0)
{
const FTransform& ChildGlobal = TransformComposer->GetGlobalTransform(Children[0]);
Direction = (ChildGlobal.GetLocation() - BoneGlobal.GetLocation()).GetSafeNormal();
}
if (Direction.IsNearlyZero())
{
return BoneGlobal;
}
// compute the secondary target based on the plane formed by the bones if needed
if (InOptions.bUsePlaneAsSecondary)
{
const FVector SecondaryDirection = (BoneGlobal.GetLocation() - ParentGlobal.GetLocation()).GetSafeNormal();
if (!SecondaryDirection.IsNearlyZero())
{
const bool bComputePlane = (FMath::Abs(FVector::DotProduct(Direction, SecondaryDirection)) - 1.0f) < KINDA_SMALL_NUMBER;
if (bComputePlane)
{ // use the plane normal as the secondary target, otherwise use InOptions' SecondaryTarget
FOrientOptions Options = InOptions;
Options.SecondaryTarget = FVector::CrossProduct(Direction, SecondaryDirection);
return Options.OrientTransform(Direction, BoneGlobal);
}
}
}
return InOptions.OrientTransform(Direction, BoneGlobal);
};
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
TArray<FName> BonesToAlign; BonesToAlign.Reserve(NumBonesToOrient);
TArray<FTransform> OrientedGlobal; OrientedGlobal.Reserve(NumBonesToOrient);
for (int32 Index = 0; Index < NumBonesToOrient; ++Index)
{
const int32 BoneIndex = BonesToOrient[Index];
BonesToAlign.Add(BoneInfos[BoneIndex].Name);
OrientedGlobal.Add(GetAlignedTransform(BoneIndex));
}
// switch back to local
TArray<FTransform> Transforms; Transforms.Reserve(NumBonesToOrient);
for (int32 Index = 0; Index < BonesToAlign.Num(); Index++)
{
const FName& BoneName = BonesToAlign[Index];
const int32 BoneIndex = ReferenceSkeleton->FindRawBoneIndex(BoneName);
const int32 ParentIndex = BoneInfos[BoneIndex].ParentIndex;
const int32 ParentOrientedIndex = ParentIndex != INDEX_NONE ? BonesToAlign.IndexOfByKey(BoneInfos[ParentIndex].Name) : INDEX_NONE;
const FTransform& ParentGlobal = ParentOrientedIndex != INDEX_NONE ? OrientedGlobal[ParentOrientedIndex] :
TransformComposer->GetGlobalTransform(ParentIndex);
Transforms.Add(OrientedGlobal[Index].GetRelativeTransform(ParentGlobal));
}
if (BonesToAlign.IsEmpty())
{
UE_LOG(LogAnimation, Error, TEXT("Skeleton Modifier - Orient: No bone to orient."));
return false;
}
static constexpr bool bMoveChildren = false;
return SetBonesTransforms(BonesToAlign, Transforms, bMoveChildren);
}
void USkeletonModifier::GetBonesToOrient(
const TArray<FName>& InBonesName, const FOrientOptions& InOptions, TArray<int32>& OutBonesToOrient) const
{
OutBonesToOrient.Reset();
TSet<int32> IndicesToOrient;
auto GetBonesToOrient = [&](const int32 BoneIndex, auto&& GetBonesToOrient2) -> void
{
if (BoneIndex == INDEX_NONE)
{
return;
}
IndicesToOrient.Add(BoneIndex);
if (InOptions.bOrientChildren)
{
TArray<int32> Children; ReferenceSkeleton->GetRawDirectChildBones(BoneIndex, Children);
for (int32 ChildIndex: Children)
{
GetBonesToOrient2(ChildIndex, GetBonesToOrient2);
}
}
};
for (const FName& BoneName: InBonesName)
{
GetBonesToOrient(ReferenceSkeleton->FindRawBoneIndex(BoneName), GetBonesToOrient);
}
const int32 NumBonesToOrient = IndicesToOrient.Num();
if (NumBonesToOrient == 0)
{
return;
}
IndicesToOrient.Sort([](const int32 Index0, const int32 Index1) {return Index0 < Index1;});
OutBonesToOrient = IndicesToOrient.Array();
}
void USkeletonModifier::UpdateBoneTracker(const TArray<FMeshBoneInfo>& InOtherInfos)
{
for (int32 Index = 0; Index < BoneIndexTracker.Num(); ++Index)
{
const int32 IndexToTrack = BoneIndexTracker[Index];
if (IndexToTrack > INDEX_NONE)
{
check(InOtherInfos.IsValidIndex(IndexToTrack));
const int32 NewIndex = ReferenceSkeleton->FindRawBoneIndex(InOtherInfos[IndexToTrack].Name);
BoneIndexTracker[Index] = NewIndex;
}
}
}
FName USkeletonModifier::GetUniqueName(const FName InBoneName, const TArray<FName>& InBoneNames) const
{
if (!ReferenceSkeleton || InBoneName == NAME_None)
{
return NAME_None;
}
static constexpr TCHAR Underscore = '_';
static constexpr TCHAR Hashtag = '#';
int32 LastHashtag = INDEX_NONE, LastDigit = INDEX_NONE;
FString InBoneNameStr = InBoneName.ToString();
// 1. Remove white spaces from start / end
InBoneNameStr.TrimStartAndEndInline();
const int32 NameLength = InBoneNameStr.Len();
// 2. Sanitize name: remove unwanted characters and get padding info from hashtags or digits
auto SanitizedName = [&]()
{
bool bHasAnyGoodChar = false;
for (int32 Index = 0; Index < NameLength; ++Index)
{
TCHAR& C = InBoneNameStr[Index];
const bool bIsAlphaOrUnderscore = FChar::IsAlpha(C) || (C == Underscore);
const bool bIsDigit = FChar::IsDigit(C);
if (bIsDigit)
{
LastDigit = Index;
}
const bool bIsHashtag = C == Hashtag;
if (bIsHashtag)
{
LastHashtag = Index;
}
const bool bGoodChar = bIsAlphaOrUnderscore || bIsHashtag || bIsDigit;
bHasAnyGoodChar |= bGoodChar;
if (!bGoodChar)
{
C = Underscore;
}
}
return bHasAnyGoodChar;
};
// 3. Early exit if none of the character is good to use
const bool bHasAnyGoodChar = SanitizedName();
if (!bHasAnyGoodChar)
{
return NAME_None;
}
// 4. if we found a padding, check if there are digits before/after to grow it if needed (ei. joint_##10_left)
// note that # takes priority here
int32 EndPadding = LastHashtag != INDEX_NONE ? LastHashtag : LastDigit != INDEX_NONE ? LastDigit : INDEX_NONE;
int32 StartPadding = EndPadding;
FString PaddingStr;
if (EndPadding != INDEX_NONE)
{
// find # or digit before EndPadding
bool bIsHashtag = InBoneNameStr[StartPadding] == Hashtag, bIsDigit = FChar::IsDigit(InBoneNameStr[StartPadding]);
while (StartPadding > INDEX_NONE && (bIsHashtag || bIsDigit))
{
--StartPadding;
if (StartPadding > INDEX_NONE)
{
bIsHashtag = InBoneNameStr[StartPadding] == Hashtag;
bIsDigit = FChar::IsDigit(InBoneNameStr[StartPadding]);
}
}
// find # or digit after StartPadding
StartPadding = StartPadding+1;
EndPadding = StartPadding;
bIsHashtag = InBoneNameStr[EndPadding] == Hashtag, bIsDigit = FChar::IsDigit(InBoneNameStr[EndPadding]);
while (EndPadding < NameLength && (bIsHashtag || bIsDigit))
{
++EndPadding;
if (EndPadding < NameLength)
{
bIsHashtag = InBoneNameStr[EndPadding] == Hashtag;
bIsDigit = FChar::IsDigit(InBoneNameStr[EndPadding]);
}
}
EndPadding -= 1;
// store the padding string
if (EndPadding >= StartPadding)
{
PaddingStr = InBoneNameStr.Mid(StartPadding, EndPadding-StartPadding+1);
}
// replace any # with zeros
static constexpr TCHAR Zero = '0';
InBoneNameStr.ReplaceInline(&Hashtag, &Zero);
PaddingStr.ReplaceInline(&Hashtag, &Zero);
}
// 5. prepare prefix, suffix and padding
FString Prefix = InBoneNameStr;
FString Suffix;
int32 CurrentIndex = 1;
if (StartPadding != INDEX_NONE)
{
CurrentIndex = PaddingStr.IsEmpty() ? INDEX_NONE : FCString::Atoi(*PaddingStr);
if (CurrentIndex == 0)
{
PaddingStr[PaddingStr.Len()-1] = '1';
CurrentIndex = 1;
}
Prefix = InBoneNameStr.Left(StartPadding);
Suffix = InBoneNameStr.RightChop(EndPadding+1);
}
// check for availability in both the reference skeleton and the names that are going to be added
auto IsNameAvailable = [&](const FString& InNameStr)
{
const FName Name(*InNameStr);
const int32 Index = ReferenceSkeleton->FindRawBoneIndex(Name);
if (Index != INDEX_NONE)
{
if (!ReferenceSkeleton->GetBoneName(Index).IsEqual(Name, ENameCase::CaseSensitive))
{
const bool bContainsByPredicate = InBoneNames.ContainsByPredicate([Name](const FName& BoneName)
{
return BoneName.IsEqual(Name, ENameCase::CaseSensitive);
});
if (!bContainsByPredicate)
{
return true;
}
}
return false;
}
if (InBoneNames.Contains(Name))
{
return false;
}
return true;
};
// 6. build the new unique name
FString OutBoneNameStr = Prefix + PaddingStr + Suffix;
while (!IsNameAvailable(OutBoneNameStr))
{
// increment the index
FString NewIncrement = PaddingStr.FromInt(CurrentIndex++);
// switch this new index into a padding str
const int32 IncrementLen = PaddingStr.Len();
const int32 NewIncrementLen = NewIncrement.Len();
if (NewIncrementLen < IncrementLen)
{
int32 Index = 0;
while (Index < NewIncrementLen)
{
PaddingStr[IncrementLen-1-Index] = NewIncrement[NewIncrementLen-1-Index];
Index++;
}
}
else
{
PaddingStr = NewIncrement;
}
// form the new name
OutBoneNameStr = Prefix + PaddingStr + Suffix;
}
return FName(*OutBoneNameStr);
}
const FReferenceSkeleton& USkeletonModifier::GetReferenceSkeleton() const
{
static const FReferenceSkeleton Dummy;
return ReferenceSkeleton ? *ReferenceSkeleton.Get() : Dummy;
}
const TArray<int32>& USkeletonModifier::GetBoneIndexTracker() const
{
return BoneIndexTracker;
}
FTransform USkeletonModifier::GetBoneTransform(const FName InBoneName, const bool bGlobal) const
{
if (!ReferenceSkeleton.IsValid())
{
return FTransform::Identity;
}
return GetTransform(ReferenceSkeleton->FindRawBoneIndex(InBoneName), bGlobal);
}
FName USkeletonModifier::GetParentName(const FName InBoneName) const
{
if (!IsReferenceSkeletonValid())
{
return NAME_None;
}
const int32 BoneIndex = ReferenceSkeleton->FindBoneIndex(InBoneName);
if (BoneIndex == INDEX_NONE)
{
return NAME_None;
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
const int32 ParentIndex = BoneInfos[BoneIndex].ParentIndex;
return (ParentIndex > INDEX_NONE) ? BoneInfos[ParentIndex].Name : NAME_None;
}
TArray<FName> USkeletonModifier::GetChildrenNames(const FName InBoneName, const bool bRecursive) const
{
TArray<FName> ChildrenNames;
if (!IsReferenceSkeletonValid())
{
return ChildrenNames;
}
const int32 BoneIndex = ReferenceSkeleton->FindRawBoneIndex(InBoneName);
if (BoneIndex == INDEX_NONE)
{
return ChildrenNames;
}
TArray<int32> ChildrenIndices;
if (bRecursive)
{
auto GetChildren = [&](const int32 InBoneIndex, auto&& GetChildren) -> void
{
TArray<int32> Children; ReferenceSkeleton->GetRawDirectChildBones(InBoneIndex, Children);
ChildrenIndices.Append(Children);
for (int32 ChildIndex: Children)
{
GetChildren(ChildIndex, GetChildren);
}
};
GetChildren(BoneIndex, GetChildren);
}
else
{
ReferenceSkeleton->GetRawDirectChildBones(BoneIndex, ChildrenIndices);
}
ChildrenNames.Reserve(ChildrenIndices.Num());
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
Algo::Transform(ChildrenIndices, ChildrenNames, [&BoneInfos](int32 BoneIndex) { return BoneInfos[BoneIndex].Name; });
return ChildrenNames;
}
TArray<FName> USkeletonModifier::GetAllBoneNames() const
{
TArray<FName> BoneNames;
if (!IsReferenceSkeletonValid())
{
return BoneNames;
}
const TArray<FMeshBoneInfo>& BoneInfos = ReferenceSkeleton->GetRawRefBoneInfo();
BoneNames.Reserve(BoneInfos.Num());
Algo::Transform(BoneInfos, BoneNames, [](const FMeshBoneInfo& BoneInfo) { return BoneInfo.Name; });
return BoneNames;
}
const FTransform& USkeletonModifier::GetTransform(const int32 InBoneIndex, const bool bGlobal) const
{
if (!ReferenceSkeleton.IsValid())
{
return FTransform::Identity;
}
if (bGlobal)
{
return TransformComposer.IsValid() ? TransformComposer->GetGlobalTransform(InBoneIndex) : FTransform::Identity;
}
const TArray<FTransform>& LocalTransforms = ReferenceSkeleton->GetRawRefBonePose();
return LocalTransforms.IsValidIndex(InBoneIndex) ? LocalTransforms[InBoneIndex] : FTransform::Identity;
}