Unreal動畫實(shí)時(shí)重定向的源碼分析

基本原理

每個(gè)動畫(AnimSequence)要指定一個(gè)重定向源(RetargetSource)鸠蚪,然后實(shí)時(shí)的時(shí)候根據(jù)重定向源的TPose和當(dāng)前角色的TPose進(jìn)行重定向的計(jì)算。

實(shí)時(shí)重定向的計(jì)算方法又分為五種: Animation, Skeleton, AnimationScale, AnimationRelative, OrientAndScale

骨骼重定向方法的設(shè)置粒度是骨骼級別萨螺,具體使用方法參見官方文檔

官方文檔:
https://docs.unrealengine.com/en-US/Engine/Animation/AnimationRetargeting/index.html

我印象中的常用操作饲鄙,通常有問題都是用以下操作解決:

  • 某個(gè)動畫重定向結(jié)果不對:在動畫編輯器里指定下重定向源

  • 某根骨骼的效果結(jié)果不對:在骨骼樹窗口里更改此骨骼的重定向設(shè)置

  • 新角色的動畫勺届,需要在最大化骨架編輯器里新增一下新角色的重定向源

  • 雙手武器重定向后廊散,掛接效果不對歹垫。這時(shí)候可能就需要ik了耸别,ik和重定向是一個(gè)搭檔。

實(shí)時(shí)重定向源碼

UE中獲取任何的骨骼動畫數(shù)據(jù)县钥,都會調(diào)用 UAnimSequence::GetBonePose

GetBonePose就像古代的關(guān)隘,連接動畫邏輯(動畫樹慈迈、Montage等)與動畫數(shù)據(jù)

壓縮動畫的重定向

壓縮動畫的重定向核心代碼就直接在GetBonePose函數(shù)的末尾

由于壓縮動畫重定向和非壓縮動畫的重定向的原理是一樣的若贮,只是讀取動畫數(shù)據(jù)的方式不同,所以重定向源碼分析放到下節(jié)

非壓縮動畫的重定向

GetBonePose函數(shù)并沒有直接處理非壓縮動畫的重定向痒留,而是通過如下調(diào)用棧:

BuildPoseFromRawData -> RetargetBoneTransform -> FAnimationRuntime::RetargetBoneTransform

所以非壓縮動畫的重定向核心邏輯如下

void FAnimationRuntime::RetargetBoneTransform(const USkeleton* MySkeleton, const FName& RetargetSource, FTransform& BoneTransform, const int32 SkeletonBoneIndex, const FCompactPoseBoneIndex& BoneIndex, const FBoneContainer& RequiredBones, const bool bIsBakedAdditive)
{
    if (MySkeleton)
    {
        switch (MySkeleton->GetBoneTranslationRetargetingMode(SkeletonBoneIndex))
        {
            case EBoneTranslationRetargetingMode::AnimationScaled:
            {
                // @todo - precache that in FBoneContainer when we have SkeletonIndex->TrackIndex mapping. So we can just apply scale right away.
                const TArray<FTransform>& SkeletonRefPoseArray = MySkeleton->GetRefLocalPoses(RetargetSource);
                const float SourceTranslationLength = SkeletonRefPoseArray[SkeletonBoneIndex].GetTranslation().Size();
                if (SourceTranslationLength > KINDA_SMALL_NUMBER)
                {
                    const float TargetTranslationLength = RequiredBones.GetRefPoseTransform(BoneIndex).GetTranslation().Size();
                    BoneTransform.ScaleTranslation(TargetTranslationLength / SourceTranslationLength);
                }
                break;
            }

            case EBoneTranslationRetargetingMode::Skeleton:
            {
                BoneTransform.SetTranslation(bIsBakedAdditive ? FVector::ZeroVector : RequiredBones.GetRefPoseTransform(BoneIndex).GetTranslation());
                break;
            }

            case EBoneTranslationRetargetingMode::AnimationRelative:
            {
                // With baked additive animations, Animation Relative delta gets canceled out, so we can skip it.
                // (A1 + Rel) - (A2 + Rel) = A1 - A2.
                if (!bIsBakedAdditive)
                {
                    const TArray<FTransform>& AuthoredOnRefSkeleton = MySkeleton->GetRefLocalPoses(RetargetSource);
                    const TArray<FTransform>& PlayingOnRefSkeleton = RequiredBones.GetRefPoseCompactArray();

                    const FTransform& RefPoseTransform = RequiredBones.GetRefPoseTransform(BoneIndex);

                    // Apply the retargeting as if it were an additive difference between the current skeleton and the retarget skeleton. 
                    BoneTransform.SetRotation(BoneTransform.GetRotation() * AuthoredOnRefSkeleton[SkeletonBoneIndex].GetRotation().Inverse() * RefPoseTransform.GetRotation());
                    BoneTransform.SetTranslation(BoneTransform.GetTranslation() + (RefPoseTransform.GetTranslation() - AuthoredOnRefSkeleton[SkeletonBoneIndex].GetTranslation()));
                    BoneTransform.SetScale3D(BoneTransform.GetScale3D() * (RefPoseTransform.GetScale3D() * AuthoredOnRefSkeleton[SkeletonBoneIndex].GetSafeScaleReciprocal(AuthoredOnRefSkeleton[SkeletonBoneIndex].GetScale3D())));
                    BoneTransform.NormalizeRotation();
                }
                break;
            }

            case EBoneTranslationRetargetingMode::OrientAndScale:
            {
                if (!bIsBakedAdditive)
                {
                    const FRetargetSourceCachedData& RetargetSourceCachedData = RequiredBones.GetRetargetSourceCachedData(RetargetSource);
                    const TArray<FOrientAndScaleRetargetingCachedData>& OrientAndScaleDataArray = RetargetSourceCachedData.OrientAndScaleData;
                    const TArray<int32>& CompactPoseIndexToOrientAndScaleIndex = RetargetSourceCachedData.CompactPoseIndexToOrientAndScaleIndex;

                    // If we have any cached retargeting data.
                    if ((OrientAndScaleDataArray.Num() > 0) && (CompactPoseIndexToOrientAndScaleIndex.Num() == RequiredBones.GetCompactPoseNumBones()))
                    {
                        const int32 OrientAndScaleIndex = CompactPoseIndexToOrientAndScaleIndex[BoneIndex.GetInt()];
                        if (OrientAndScaleIndex != INDEX_NONE)
                        {
                            const FOrientAndScaleRetargetingCachedData& OrientAndScaleData = OrientAndScaleDataArray[OrientAndScaleIndex];
                            const FVector AnimatedTranslation = BoneTransform.GetTranslation();

                            // If Translation is not animated, we can just copy the TargetTranslation. No retargeting needs to be done.
                            const FVector NewTranslation = (AnimatedTranslation - OrientAndScaleData.SourceTranslation).IsNearlyZero(BONE_TRANS_RT_ORIENT_AND_SCALE_PRECISION) ?
                                OrientAndScaleData.TargetTranslation :
                                OrientAndScaleData.TranslationDeltaOrient.RotateVector(AnimatedTranslation) * OrientAndScaleData.TranslationScale;

                            BoneTransform.SetTranslation(NewTranslation);
                        }
                    }
                }
                break;
            }
        }
    }
}

這個(gè)函數(shù)只是一根骨骼的重定向計(jì)算谴麦。

當(dāng)前角色的TPose就存在RequiredBones里。

重定向源的TPose都是存在最大化骨架MySkeleton中的伸头,根據(jù)AnimSequence.RetargetSource來獲取當(dāng)前的重定向源數(shù)據(jù)

大部分的實(shí)時(shí)重定向的方法主要改的就是骨骼的translation匾效,scale和rotation不會動,AnimationRelative模式例外

  • Animation: 純動畫恤磷,不做任何重定向
  • Skeleton: 直接用角色TPose的translation
  • AnimationScale: 用動畫的Translation面哼,但是會根據(jù)當(dāng)前角色的TPose和RetargetSource的TPose的比例對Translation進(jìn)行縮放。 和AnimationRelative相比扫步,delta是用除法算出來的魔策,且只改Translation
  • AnimationRelative: 用動畫的Translation,scale河胎,rotation闯袒,但是會根據(jù)當(dāng)前角色的TPose和RetargetSource的TPose的差值對動畫的prs進(jìn)行增減。和AnimationScale相比游岳,delta是用減法算出來的政敢。
  • OrientAndScale: 用動畫的Translation,實(shí)時(shí)會根據(jù)OrientAndScaleData的信息對Translation進(jìn)行變換

OrientAndScale模式下的OrientAndScaleData的計(jì)算

  • OrientAndScaleData.TranslationDeltaOrient是一個(gè)四元數(shù)旋轉(zhuǎn): 是從重定向源的骨骼translation(SourceSkelTransDir)到當(dāng)前角色TPose的骨骼Translation(TargetSkelTransDir)的一個(gè)旋轉(zhuǎn)
  • OrientAndScaleData.TranslationScale是一個(gè)scale的除法: 當(dāng)前角色TPose的骨骼Translation的長度(TargetSkelTransLength) / 重定向源的骨骼translation的長度(SourceSkelTransLength)
const FRetargetSourceCachedData& FBoneContainer::GetRetargetSourceCachedData(const FName& InRetargetSourceName) const
{
    FRetargetSourceCachedData* RetargetSourceCachedData = RetargetSourceCachedDataLUT.Find(InRetargetSourceName);
    if (!RetargetSourceCachedData)
    {
        RetargetSourceCachedData = &RetargetSourceCachedDataLUT.Add(InRetargetSourceName);

        // Build Cached Data for OrientAndScale retargeting.

        const TArray<FTransform>& AuthoredOnRefSkeleton = AssetSkeleton->GetRefLocalPoses(InRetargetSourceName);
        const TArray<FTransform>& PlayingOnRefSkeleton = GetRefPoseCompactArray();
        const int32 CompactPoseNumBones = GetCompactPoseNumBones();

        RetargetSourceCachedData->CompactPoseIndexToOrientAndScaleIndex.Reset();

        for (int32 CompactBoneIndex = 0; CompactBoneIndex < CompactPoseNumBones; CompactBoneIndex++)
        {
            const int32& SkeletonBoneIndex = CompactPoseToSkeletonIndex[CompactBoneIndex];

            if (AssetSkeleton->GetBoneTranslationRetargetingMode(SkeletonBoneIndex) == EBoneTranslationRetargetingMode::OrientAndScale)
            {
                const FVector SourceSkelTrans = AuthoredOnRefSkeleton[SkeletonBoneIndex].GetTranslation();
                const FVector TargetSkelTrans = PlayingOnRefSkeleton[CompactBoneIndex].GetTranslation();

                // If translations are identical, we don't need to do any retargeting
                if (!SourceSkelTrans.Equals(TargetSkelTrans, BONE_TRANS_RT_ORIENT_AND_SCALE_PRECISION))
                {
                    const float SourceSkelTransLength = SourceSkelTrans.Size();
                    const float TargetSkelTransLength = TargetSkelTrans.Size();

                    // this only works on non zero vectors.
                    if (!FMath::IsNearlyZero(SourceSkelTransLength * TargetSkelTransLength))
                    {
                        const FVector SourceSkelTransDir = SourceSkelTrans / SourceSkelTransLength;
                        const FVector TargetSkelTransDir = TargetSkelTrans / TargetSkelTransLength;

                        const FQuat DeltaRotation = FQuat::FindBetweenNormals(SourceSkelTransDir, TargetSkelTransDir);
                        const float Scale = TargetSkelTransLength / SourceSkelTransLength;
                        const int32 OrientAndScaleIndex = RetargetSourceCachedData->OrientAndScaleData.Add(FOrientAndScaleRetargetingCachedData(DeltaRotation, Scale, SourceSkelTrans, TargetSkelTrans));

                        // initialize CompactPoseBoneIndex to OrientAndScale Index LUT on demand
                        if (RetargetSourceCachedData->CompactPoseIndexToOrientAndScaleIndex.Num() == 0)
                        {
                            RetargetSourceCachedData->CompactPoseIndexToOrientAndScaleIndex.Init(INDEX_NONE, CompactPoseNumBones);
                        }

                        RetargetSourceCachedData->CompactPoseIndexToOrientAndScaleIndex[CompactBoneIndex] = OrientAndScaleIndex;
                    }
                }
            }
        }
    }

    return *RetargetSourceCachedData;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胚迫,一起剝皮案震驚了整個(gè)濱河市喷户,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晌区,老刑警劉巖摩骨,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件通贞,死亡現(xiàn)場離奇詭異,居然都是意外死亡恼五,警方通過查閱死者的電腦和手機(jī)昌罩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灾馒,“玉大人茎用,你說我怎么就攤上這事〔锹蓿” “怎么了轨功?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長容达。 經(jīng)常有香客問我古涧,道長,這世上最難降的妖魔是什么花盐? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任羡滑,我火速辦了婚禮,結(jié)果婚禮上算芯,老公的妹妹穿的比我還像新娘柒昏。我一直安慰自己,他們只是感情好熙揍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布职祷。 她就那樣靜靜地躺著,像睡著了一般届囚。 火紅的嫁衣襯著肌膚如雪有梆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天奖亚,我揣著相機(jī)與錄音淳梦,去河邊找鬼。 笑死昔字,一個(gè)胖子當(dāng)著我的面吹牛爆袍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播作郭,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陨囊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了夹攒?” 一聲冷哼從身側(cè)響起蜘醋,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咏尝,沒想到半個(gè)月后压语,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啸罢,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年胎食,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扰才。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厕怜,死狀恐怖衩匣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粥航,我是刑警寧澤琅捏,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站递雀,受9級特大地震影響柄延,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缀程,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一拦焚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杠输,春花似錦、人聲如沸秕衙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽据忘。三九已至鹦牛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勇吊,已是汗流浹背曼追。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汉规,地道東北人礼殊。 一個(gè)月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像针史,于是被迫代替她去往敵國和親晶伦。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容