--- 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 Mixamo_PreserveComponentSpacePose_BoneNames = { "Head", "LeftToeBase", "RightToeBase" #ifdef MAR_UPPERARMS_PRESERVECS_EXPERIMENTAL_ENABLE_ ,"RightShoulder" ,"RightArm" ,"LeftShoulder" ,"LeftArm"#endif }; static const TArray> Mixamo_ParentChildBoneNamesToBypassOneChildConstraint = { {"LeftUpLeg", "LeftLeg"}, {"LeftLeg", "LeftFoot"}, {"LeftFoot", "LeftToeBase"}, {"LeftToeBase", "LeftToe_End"}, {"RightUpLeg", "RightLeg"}, {"RightLeg", "RightFoot"}, {"RightFoot", "RightToeBase"}, {"RightToeBase", "RightToe_End"}, {"Hips", "Spine"}, // Heuristic to try to align better the part. {"Spine", "Spine1"}, {"Spine1", "Spine2"}, {"Spine2", "Neck"}, // Heuristic to try to align better the part. {"Neck", "Head"}, {"Head", "HeadTop_End"}, {"LeftShoulder", "LeftArm"}, {"LeftArm", "LeftForeArm"}, {"LeftForeArm", "LeftHand"}, {"LeftHand", "LeftHandMiddle1"}, // Heuristic to try to align better the part. {"LeftHandIndex1", "LeftHandIndex2"}, {"LeftHandIndex2", "LeftHandIndex3"}, {"LeftHandIndex3", "LeftHandIndex4"}, {"LeftHandMiddle1", "LeftHandMiddle2"}, {"LeftHandMiddle2", "LeftHandMiddle3"}, {"LeftHandMiddle3", "LeftHandMiddle4"}, {"LeftHandPinky1", "LeftHandPinky2"}, {"LeftHandPinky2", "LeftHandPinky3"}, {"LeftHandPinky3", "LeftHandPinky4"}, {"LeftHandRing1", "LeftHandRing2"}, {"LeftHandRing2", "LeftHandRing3"}, {"LeftHandRing3", "LeftHandRing4"}, {"LeftHandThumb1", "LeftHandThumb2"}, {"LeftHandThumb2", "LeftHandThumb3"}, {"LeftHandThumb3", "LeftHandThumb4"}, {"RightShoulder", "RightArm"}, {"RightArm", "RightForeArm"}, {"RightForeArm", "RightHand"}, {"RightHand", "RightHandMiddle1"}, // Heuristic to try to align better the part. {"RightHandIndex1", "RightHandIndex2"}, {"RightHandIndex2", "RightHandIndex3"}, {"RightHandIndex3", "RightHandIndex4"}, {"RightHandMiddle1", "RightHandMiddle2"}, {"RightHandMiddle2", "RightHandMiddle3"}, {"RightHandMiddle3", "RightHandMiddle4"}, {"RightHandPinky1", "RightHandPinky2"}, {"RightHandPinky2", "RightHandPinky3"}, {"RightHandPinky3", "RightHandPinky4"}, {"RightHandRing1", "RightHandRing2"}, {"RightHandRing2", "RightHandRing3"}, {"RightHandRing3", "RightHandRing4"}, {"RightHandThumb1", "RightHandThumb2"}, {"RightHandThumb2", "RightHandThumb3"}, {"RightHandThumb3", "RightHandThumb4"} }; 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 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& srcRtgChains = SourceIKRig->GetRetargetChains(); const TArray& 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(); SourceIKRigProcessor->Initialize(SourceIKRig,SourceIKRig->GetPreviewMesh()); UIKRigProcessor* TargetIKRigProcessor=NewObject(); TargetIKRigProcessor->Initialize(TargetIKRig,TargetIKRig->GetPreviewMesh()); const FIKRigSkeleton& srcRigSkeleton = SourceIKRigProcessor->GetSkeleton(); const FIKRigSkeleton& tgtRigSkeleton = TargetIKRigProcessor->GetSkeleton(); for(TObjectPtr 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 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{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; } ```