BlueRoseNote/03-UnrealEngine/Animation/UE5商城动画重定向插件笔记.md

377 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: UE5商城动画重定向插件笔记
date: 2023-09-05 12:02:29
excerpt:
tags:
- AnimationRetargeting
rating: ⭐
---
# MixamoAnimationRetargeting
主要逻辑位于FMixamoSkeletonRetargeter
- UE4MannequinToMixamo_BoneNamesMapping
- UE4MannequinToMixamo_ChainNamesMapping
- UE5MannequinToMixamo_BoneNamesMapping
- UE5MannequinToMixamo_ChainNamesMapping
重定向逻辑位于FMixamoSkeletonRetargeter::Retarget()
## 生成TPose
```c++
static const TArray<FName> Mixamo_PreserveComponentSpacePose_BoneNames = {
"Head",
"LeftToeBase",
"RightToeBase"
#ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_
,"RightShoulder"
,"RightArm" ,"LeftShoulder" ,"LeftArm"#endif
};
static const TArray<TPair<FName, FName>> Mixamo_ParentChildBoneNamesToBypassOneChildConstraint = {
{"LeftUpLeg", "LeftLeg"},
{"LeftLeg", "LeftFoot"},
{"LeftFoot", "LeftToeBase"},
{"LeftToeBase", "LeftToe_End"},
{"RightUpLeg", "RightLeg"},
{"RightLeg", "RightFoot"},
{"RightFoot", "RightToeBase"},
{"RightToeBase", "RightToe_End"},
{"Hips", "Spine"}, // Heuristic to try to align better the part.
{"Spine", "Spine1"},
{"Spine1", "Spine2"},
{"Spine2", "Neck"}, // Heuristic to try to align better the part.
{"Neck", "Head"},
{"Head", "HeadTop_End"},
{"LeftShoulder", "LeftArm"},
{"LeftArm", "LeftForeArm"},
{"LeftForeArm", "LeftHand"},
{"LeftHand", "LeftHandMiddle1"}, // Heuristic to try to align better the part.
{"LeftHandIndex1", "LeftHandIndex2"},
{"LeftHandIndex2", "LeftHandIndex3"},
{"LeftHandIndex3", "LeftHandIndex4"},
{"LeftHandMiddle1", "LeftHandMiddle2"},
{"LeftHandMiddle2", "LeftHandMiddle3"},
{"LeftHandMiddle3", "LeftHandMiddle4"},
{"LeftHandPinky1", "LeftHandPinky2"},
{"LeftHandPinky2", "LeftHandPinky3"},
{"LeftHandPinky3", "LeftHandPinky4"},
{"LeftHandRing1", "LeftHandRing2"},
{"LeftHandRing2", "LeftHandRing3"},
{"LeftHandRing3", "LeftHandRing4"},
{"LeftHandThumb1", "LeftHandThumb2"},
{"LeftHandThumb2", "LeftHandThumb3"},
{"LeftHandThumb3", "LeftHandThumb4"},
{"RightShoulder", "RightArm"},
{"RightArm", "RightForeArm"},
{"RightForeArm", "RightHand"},
{"RightHand", "RightHandMiddle1"}, // Heuristic to try to align better the part.
{"RightHandIndex1", "RightHandIndex2"},
{"RightHandIndex2", "RightHandIndex3"},
{"RightHandIndex3", "RightHandIndex4"},
{"RightHandMiddle1", "RightHandMiddle2"},
{"RightHandMiddle2", "RightHandMiddle3"},
{"RightHandMiddle3", "RightHandMiddle4"},
{"RightHandPinky1", "RightHandPinky2"},
{"RightHandPinky2", "RightHandPinky3"},
{"RightHandPinky3", "RightHandPinky4"},
{"RightHandRing1", "RightHandRing2"},
{"RightHandRing2", "RightHandRing3"},
{"RightHandRing3", "RightHandRing4"},
{"RightHandThumb1", "RightHandThumb2"},
{"RightHandThumb2", "RightHandThumb3"},
{"RightHandThumb3", "RightHandThumb4"}
};
RetargetBasePose(
SkeletalMeshes,
ReferenceSkeleton,
Mixamo_PreserveComponentSpacePose_BoneNames,
UEMannequinToMixamo_BoneNamesMapping.GetInverseMapper(),
Mixamo_ParentChildBoneNamesToBypassOneChildConstraint,
/*bApplyPoseToRetargetBasePose=*/true,
UIKRetargeterController::GetController(IKRetargeter_UEMannequinToMixamo)
);
```
## 判断骨骼结构是否符合要求
```c++
bool FMixamoSkeletonRetargeter::IsMixamoSkeleton(const USkeleton * Skeleton) const
{
// We consider a Skeleton "coming from Mixamo" if it has at least X% of the expected bones.
const float MINIMUM_MATCHING_PERCENTAGE = .75f;
// Convert the array of expected bone names (TODO: cache it...).
TArray<FName> BoneNames;
UE4MannequinToMixamo_BoneNamesMapping.GetDestination(BoneNames);
// Look for and count the known Mixamo bones (see comments on IndexLastCheckedMixamoBone and UEMannequinToMixamo_BonesMapping).
constexpr int32 NumBones = (IndexLastCheckedMixamoBone + 1) / 2;
BoneNames.SetNum(NumBones);
FSkeletonMatcher SkeletonMatcher(BoneNames, MINIMUM_MATCHING_PERCENTAGE);
return SkeletonMatcher.IsMatching(Skeleton);
}
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;
}
```
enum class ETargetSkeletonType
{
ST_UNKNOWN = 0,
ST_UE4_MANNEQUIN,
ST_UE5_MANNEQUIN,
ST_SIZE
};
static const char* const kUE4MannequinToMixamo_BoneNamesMapping[] = {
// UE Mannequin bone name MIXAMO bone name
"root", "root",
"pelvis", "Hips",
"spine_01", "Spine",
"spine_02", "Spine1",
"spine_03", "Spine2",
"neck_01", "Neck",
"head", "head",
"clavicle_l", "LeftShoulder",
"upperarm_l", "LeftArm",
"lowerarm_l", "LeftForeArm",
"hand_l", "LeftHand",
"clavicle_r", "RightShoulder",
"upperarm_r", "RightArm",
"lowerarm_r", "RightForeArm",
"hand_r", "RightHand",
"thigh_l", "LeftUpLeg",
"calf_l", "LeftLeg",
"foot_l", "LeftFoot",
"ball_l", "LeftToeBase",
"thigh_r", "RightUpLeg",
"calf_r", "RightLeg",
"foot_r", "RightFoot",
"ball_r", "RightToeBase",
// From here, ignored to determine if a skeleton is from Mixamo.
// From here, ignored to determine if a skeleton is from UE Mannequin. "index_01_l", "LeftHandIndex1",
"index_02_l", "LeftHandIndex2",
"index_03_l", "LeftHandIndex3",
"middle_01_l", "LeftHandMiddle1",
"middle_02_l", "LeftHandMiddle2",
"middle_03_l", "LeftHandMiddle3",
"pinky_01_l", "LeftHandPinky1",
"pinky_02_l", "LeftHandPinky2",
"pinky_03_l", "LeftHandPinky3",
"ring_01_l", "LeftHandRing1",
"ring_02_l", "LeftHandRing2",
"ring_03_l", "LeftHandRing3",
"thumb_01_l", "LeftHandThumb1",
"thumb_02_l", "LeftHandThumb2",
"thumb_03_l", "LeftHandThumb3",
"index_01_r", "RightHandIndex1",
"index_02_r", "RightHandIndex2",
"index_03_r", "RightHandIndex3",
"middle_01_r", "RightHandMiddle1",
"middle_02_r", "RightHandMiddle2",
"middle_03_r", "RightHandMiddle3",
"pinky_01_r", "RightHandPinky1",
"pinky_02_r", "RightHandPinky2",
"pinky_03_r", "RightHandPinky3",
"ring_01_r", "RightHandRing1",
"ring_02_r", "RightHandRing2",
"ring_03_r", "RightHandRing3",
"thumb_01_r", "RightHandThumb1",
"thumb_02_r", "RightHandThumb2",
"thumb_03_r", "RightHandThumb3",
// Un-mapped bones (at the moment). Here for reference.
//"lowerarm_twist_01_l", nullptr, //"upperarm_twist_01_l", nullptr, //"lowerarm_twist_01_r", nullptr, //"upperarm_twist_01_r", nullptr, //"calf_twist_01_l", nullptr, //"thigh_twist_01_l", nullptr, //"calf_twist_01_r", nullptr, //"thigh_twist_01_r", nullptr, //"ik_foot_root", nullptr, //"ik_foot_l", nullptr, //"ik_foot_r", nullptr, //"ik_hand_root", nullptr, //"ik_hand_gun", nullptr, //"ik_hand_l", nullptr, //"ik_hand_r", nullptr,};
# EasyPose
主要的逻辑都是在MatchPose()中作者还是编写几个辅助函数以及IKRigRetargetChainDetail结构体。
定义srcRtgSkeleton、tgtRtgSkeleton之外还定义了orgTgtRtgSkeleton主要是为了在最后结算最终的Offset。UE的重定向Pose数据使用分离的方式存储Maya也是这样但UE输出的FBX却不是分为RefPose与RetargetPoseOffset。
- 初始化数据
- 初始化IKRigRetargetChainDetail srcIkRigRtgChainDetail & IKRigRetargetChainDetail tgtIkRigRtgChainDetail结构体以用于之后的计算
- 遍历骨骼链
- 根据当前Source & Target ID计算当前长度的百分比如果TargetID > SourceID则**srcId++**,并且跳过当前循环。(主要是考虑到骨骼链内骨骼数量不对等的情况)
- 取得当前SourceBoneLocation以及SourceNextBoneLocation来计算Direction
- 重新生成tgtRtgSkeleton、orgTgtRtgSkeleton的重定向Pose**WorldSpace**
- **之后是核心MatchPose逻辑**
- 计算Direction与TargetBoneDirection的旋转偏移转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。
- 在X 或者 Y轴对齐的情况下 计算Source ID与Target ID 对应骨骼的旋转Offset转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。
- 计算Direction与TargetBoneDirection的旋转Offset转成**LocalSpace**之后应用到tgtRtgSkeleton的对应Target骨骼上。以上上步相同
- 使用tgtRtgSkeleton、orgTgtRtgSkeleton计算最终的旋转Offset。
- 使用最终FinalyOffset 应用到 Target的IKRig 的 FIKRetargetPose。
- **tgtId++**
以下是完整的代码:
```c++
int32 UAutomationUtilityLibrary::MatchPose(UIKRetargeter *iKRetargeter) {
int totalRotationsCount = 0;
if(iKRetargeter == nullptr) return 0;
auto Target = ERetargetSourceOrTarget::Target;
auto Source = ERetargetSourceOrTarget::Source;
UIKRetargeterController *ikRtgCtrl = UIKRetargeterController::GetController(iKRetargeter);
if(ikRtgCtrl == nullptr) return 0;
const UIKRigDefinition* SourceIKRig = iKRetargeter->GetSourceIKRig();
const UIKRigDefinition* TargetIKRig = iKRetargeter->GetTargetIKRig();
const TArray<FBoneChain>& srcRtgChains = SourceIKRig->GetRetargetChains();
const TArray<FBoneChain>& tgtRtgChains = TargetIKRig->GetRetargetChains();
const FName& srcRtgRootName = SourceIKRig->GetRetargetRoot();
const FName& tgtRtgRootName = TargetIKRig->GetRetargetRoot();
FRetargetSkeleton srcRtgSkeleton;
FRetargetSkeleton tgtRtgSkeleton, orgTgtRtgSkeleton;
auto tgtRtgPoseName = iKRetargeter->GetCurrentRetargetPoseName(ERetargetSourceOrTarget::Target);
FIKRetargetPose const *tgtRtgPose = iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target);
srcRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Source),
srcRtgChains,
iKRetargeter->GetCurrentRetargetPoseName(ERetargetSourceOrTarget::Source),
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Source),
srcRtgRootName);
tgtRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Target),
tgtRtgChains,
tgtRtgPoseName,
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target),
tgtRtgRootName);
orgTgtRtgSkeleton.Initialize(
ikRtgCtrl->GetPreviewMesh(Target),
tgtRtgChains,
tgtRtgPoseName,
iKRetargeter->GetCurrentRetargetPose(ERetargetSourceOrTarget::Target),
tgtRtgRootName);
UIKRigProcessor* SourceIKRigProcessor=NewObject<UIKRigProcessor>();
SourceIKRigProcessor->Initialize(SourceIKRig,SourceIKRig->GetPreviewMesh());
UIKRigProcessor* TargetIKRigProcessor=NewObject<UIKRigProcessor>();
TargetIKRigProcessor->Initialize(TargetIKRig,TargetIKRig->GetPreviewMesh());
const FIKRigSkeleton& srcRigSkeleton = SourceIKRigProcessor->GetSkeleton();
const FIKRigSkeleton& tgtRigSkeleton = TargetIKRigProcessor->GetSkeleton();
for(TObjectPtr<URetargetChainSettings> chainSettings : iKRetargeter->GetAllChainSettings()) {
if(chainSettings == nullptr) continue;
const FBoneChain *srcBoneChain = SourceIKRig->GetRetargetChainByName(chainSettings->SourceChain);
const FBoneChain *tgtBoneChain = TargetIKRig->GetRetargetChainByName(chainSettings->TargetChain);
if(srcBoneChain == nullptr || tgtBoneChain == nullptr) continue;
auto chainRoot = FName(TEXT("Root"));
if(chainSettings->SourceChain == chainRoot || chainSettings->TargetChain == chainRoot) {
continue;
}
IKRigRetargetChainDetail srcIkRigRtgChainDetail{srcBoneChain, srcRigSkeleton, srcRtgSkeleton};
IKRigRetargetChainDetail tgtIkRigRtgChainDetail{tgtBoneChain, tgtRigSkeleton, tgtRtgSkeleton};
if(srcIkRigRtgChainDetail.ikRigRetargetMap.IsEmpty() || tgtIkRigRtgChainDetail.ikRigRetargetMap.IsEmpty()) {
//UE_LOG(AutomationFunctionLibrary, Log, TEXT("Chain Empty: %s: %d, %s: %d"), *srcIkRigRtgChainDetail.boneChain->ChainName.ToString(), srcIkRigRtgChainDetail.ikRigRetargetMap.Num(), *tgtIkRigRtgChainDetail.boneChain->ChainName.ToString(), tgtIkRigRtgChainDetail.ikRigRetargetMap.Num());
continue;
}
auto srcWatermarkLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[0].retargetId].GetTranslation();
int rotationsCount = 0;
for(int32 srcId=0,tgtId=0; srcId<srcIkRigRtgChainDetail.ikRigRetargetMap.Num()-1 && tgtId<tgtIkRigRtgChainDetail.ikRigRetargetMap.Num()-1;) {
auto [srcIkRigId, srcRtgId, srcLenToNext, srcTotalLenUntilCurrent] = srcIkRigRtgChainDetail.ikRigRetargetMap[srcId];
auto [tgtIkRigId, tgtRtgId, tgtLenToNext, tgtTotalLenUntilCurrent] = tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId];
float srcTotalLenToNextPercent = (srcTotalLenUntilCurrent + srcLenToNext) / srcIkRigRtgChainDetail.totalLength;
float tgtTotalLenToNextPercent = (tgtTotalLenUntilCurrent + tgtLenToNext) / tgtIkRigRtgChainDetail.totalLength;
if(tgtTotalLenToNextPercent > srcTotalLenToNextPercent) {
srcId++;
continue;
} else {
auto tgtRtgBoneName = tgtRigSkeleton.BoneNames[tgtIkRigId];
auto diffLenPercentToNext = 1 - ((srcTotalLenToNextPercent - tgtTotalLenToNextPercent) * srcIkRigRtgChainDetail.totalLength / srcLenToNext);
auto srcCurrentBoneLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[srcId].retargetId].GetTranslation();
auto srcNextBoneLoc = srcRtgSkeleton.RetargetGlobalPose[srcIkRigRtgChainDetail.ikRigRetargetMap[srcId+1].retargetId].GetTranslation();
auto nextPoint = (srcNextBoneLoc - srcCurrentBoneLoc) * (diffLenPercentToNext) + srcCurrentBoneLoc;
auto direction = (nextPoint - srcWatermarkLoc).GetSafeNormal();
srcWatermarkLoc = nextPoint;
//ResetRetargetPose
ikRtgCtrl->ResetRetargetPose(tgtRtgPoseName, TArray<FName>{tgtRtgBoneName}, Target);
tgtRtgSkeleton.GenerateRetargetPose(tgtRtgPoseName, tgtRtgPose, tgtRtgRootName);
orgTgtRtgSkeleton.GenerateRetargetPose(tgtRtgPoseName, tgtRtgPose, tgtRtgRootName);
//根据骨骼链中当前SourceBone与下一个SourceBone计算出Direction世界坐标
//GetBoneDirectionOffset 调用GetBoneDirectionOffset()计算出旋转偏移再转换成LocalSpace旋转再设置给骨骼。
auto const &tgtGlobalTransform = tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId];
auto rotOffset = GetBoneDirectionOffset(tgtRtgSkeleton, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId].retargetId, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId+1].retargetId, direction);
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching direction: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched direction: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//在X 或者 Y轴对齐的情况下 计算Source与Target 对应骨骼的旋转偏差在转化成LocalSpace后再应用
rotOffset = GetXYRotationOffsetIfAligned(tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId].GetRotation(), srcRtgSkeleton.RetargetGlobalPose[srcRtgId].GetRotation());
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching XY: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched XY: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//与第一步相同
rotOffset = GetBoneDirectionOffset(tgtRtgSkeleton, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId].retargetId, tgtIkRigRtgChainDetail.ikRigRetargetMap[tgtId+1].retargetId, direction);
rotOffset = GetBoneSpaceOffset(rotOffset, tgtGlobalTransform);
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching direction 2nd: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched direction 2nd: %s"), *tgtRtgBoneName.ToString());
ApplyRotationOffset(rotOffset, tgtRtgId, tgtRtgSkeleton);
}
//计算最终的旋转offset
auto finalOffset = GetRotationDifference(orgTgtRtgSkeleton.RetargetLocalPose[tgtRtgId].GetRotation(), tgtRtgSkeleton.RetargetLocalPose[tgtRtgId].GetRotation());
if(NotEmptyOrIdentity(finalOffset)) {
rotationsCount++;
ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, finalOffset, Target);
} tgtId++;
}
}
auto [srcIkRigId, srcRtgId, srcLenToNext, srcTotalLenUntilCurrent] = srcIkRigRtgChainDetail.ikRigRetargetMap.Last();
auto [tgtIkRigId, tgtRtgId, tgtLenToNext, tgtTotalLenUntilCurrent] = tgtIkRigRtgChainDetail.ikRigRetargetMap.Last();
auto tgtRtgBoneName = tgtRigSkeleton.BoneNames[tgtIkRigId];
auto rotOffset = GetRotationOffsetIfAligned(tgtRtgSkeleton.RetargetGlobalPose[tgtRtgId].GetRotation(), srcRtgSkeleton.RetargetGlobalPose[srcRtgId].GetRotation());
//UE_LOG(AutomationFunctionLibrary, VeryVerbose, TEXT("Matching rotation: %s, %s"), *tgtRtgBoneName.ToString(), *rotOffset.ToString());
if(NotEmptyOrIdentity(rotOffset)) {
rotationsCount++;
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Matched rotation: %s"), *tgtRtgBoneName.ToString());
ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, ikRtgCtrl->GetRotationOffsetForRetargetPoseBone(tgtRtgBoneName, Target) * rotOffset, Target);
} if(rotationsCount > 0) {
//UE_LOG(AutomationFunctionLibrary, Verbose, TEXT("Chain: %s - %s: %d rotations"), *chainSettings->SourceChain.ToString(), *chainSettings->TargetChain.ToString(), rotationsCount);
totalRotationsCount += rotationsCount;
} } /*
if(totalRotationsCount > 0) { UE_LOG(AutomationFunctionLibrary, Log, TEXT("Asset \"%s\": made %d adjustments with pose: \"%s\"."), *iKRetargeter->GetName(), totalRotationsCount, *ikRtgCtrl->GetCurrentRetargetPoseName(Target).ToString()); } else { UE_LOG(AutomationFunctionLibrary, Log, TEXT("Asset \"%s\": pose \"%s\" fit already!"), *iKRetargeter->GetName(), *ikRtgCtrl->GetCurrentRetargetPoseName(Target).ToString()); } */ // ikRtgCtrl->SetRotationOffsetForRetargetPoseBone(MeshRefSkeleton.GetBoneName(EditBoneIndex), Q, ERetargetSourceOrTarget::Target);
return totalRotationsCount;
}
```