BlueRose
文章97
标签28
分类7
AdvancedLocomotionV4学习笔记(2)——AnimModifier

AdvancedLocomotionV4学习笔记(2)——AnimModifier

前言

在AdvancedLocomotionV4中大量使用同步组、AnimNotify、AnimationCurve。但那么多动画Asset都要手动添加,我肯定是拒绝的。
image

幸亏Ue4在4.16推出了AnimModifier功能以进行自动化操作。

文档地址:
https://docs.unrealengine.com/en-US/Engine/Animation/AnimModifiers/index.html

本人有幸找到了一篇相关Blog:
http://www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/

并以此为基础编写了生成LocoMotion相关同步组、AnimNotify、AnimationCurve的AnimModifier(主要还是因为作者写的东西不太行),并且已经将其放入之前写的插件中,如觉得好用请给我点赞

https://github.com/blueroseslol/BRPlugins

image

使用效果与使用说明

  • PathFilter:路径过滤,在批量处理时,只会处理指定路径的AnimSequence。(使用前需要实现修改好对应变量的默认值)
  • MotionCheckDirection:骨骼位移的判断轴向。
  • CreateBonePositionCurve:生成以Bone为名称的曲线,值为对应时间的骨骼高度。
  • CreateFootPositionCurve:创建类似AdvancedLocomotionV4中的Feet_Position曲线。
  • NormalizeCurve:将BonePosition曲线归一化。
  • FeetBones:定义脚部骨骼,一般使用foot_l与foot_r,offset为骨骼判断偏移值。
  • AnimNotfiyClass:指定需要添加的AnimNotify。
  • StepOnValue:脚着地的判断高度。
  • StepNextValue:脚离地的判断值。(一般是1~5)
  • InterpMode:BonePosition曲线的插值模式。

具体实现说明

本人已经在蓝图中做了详细的注释,所以直接去看蓝图也是没问题的。下面将介绍具体实现方式:

AnimModifier有2个需要实现的事件,分别是OnApply与OnRevert,指代了应用AnimModifier生成数据与撤销AnimModifier生成的数据。以下展示的是插件中OnApply事件的实现。(OnRevert只包含下图的3个节点)

ShouldApply

通过判断设置的PathFilter来判断是否处理AnimSequence。

RemoveAll

移除对应的Notify、Curve、Sync轨道。

创建所需轨道与取得所需数据

在进行完初始化后,按照设定的变量创建各种轨道:

按照设置的脚部骨骼进行循环,生成对应的曲线轨道,并且取得AnimSequence的时长,之后再对AnimSequence的每一帧进行逐帧处理。

通过GetBoneLocationRelativeToAtTime函数计算相对位移之后再根据的轴向取得偏移值(一般都是Z轴方向)

GetBoneLocationRelativeToAtTime

GetBoneLocationRelativeToAtTime函数的作用为,计算目标骨骼与相对骨骼之间的位移。(在骨骼路径后,将每段骨骼的Translation相乘,最后再转化为Location)

AnimModifier中的添加曲线函数AddFloatCurveKey与AddFloatCurveKeys没有提供修改曲线插值类型选项,所以我简单扩展了一下AnimationBlueprintLibrary,具体的可以参考我的插件代码。(懒得提交Pull Request了)

数据处理

因为之后需要对生成的曲线进行归一化处理,所以在红框处,我将所取得的每帧曲线数据进行存储。

白框处先是通过计算脚部骨骼的相对位移来判断脚是踩到地还是抬起来,之后再设定同步组。

蓝框处则是在判断左右脚之后设置了对应的曲线值(Feet_Position)。

这里我重写了AddFloatCurvekeyWithType函数,这样就可以给关键帧指定插值方式了。具体代码我放在本文最后了。

循环完每一帧之后

归一化曲线

添加曲线(对应骨骼名称曲线),之后将数据清零。进行下一个骨骼的循环。

最后处理

Feet_Position曲线的开头与结尾处没有关键帧,此时在进行判断后添加。

最后再执行FinalizeBoneAnimation。

AnimationBlueprintLibrary扩展

AnimModifier中的添加曲线函数AddFloatCurveKey与AddFloatCurveKeys没有提供修改曲线插值类型选项,所以我简单扩展了一下AnimationBlueprintLibrary,具体的可以参考我的插件代码。(懒得提交Pull Request了)

void UAnimBlueprintLibrary::AddFloatCurveKeysWithType(UAnimSequence* AnimationSequence, FName CurveName, const TArray<float>& Times, const TArray<float>& Values, EInterpCurveMode InterpMode)
{
    if (AnimationSequence)
    {
        if (Times.Num() == Values.Num())
        {
            AddCurveKeysInternal<float, FFloatCurve>(AnimationSequence, CurveName, Times, Values, ERawCurveTrackTypes::RCT_Float, InterpMode);
        }
        else
        {
            UE_LOG(LogAnimBlueprintLibrary, Warning, TEXT("Number of Time values %i does not match the number of Values %i in AddFloatCurveKeys"), Times.Num(), Values.Num());
        }
    }
    else
    {
        UE_LOG(LogAnimBlueprintLibrary, Warning, TEXT("Invalid Animation Sequence for AddFloatCurveKeys"));
    }

}


template <typename DataType, typename CurveClass>
void UAnimBlueprintLibrary::AddCurveKeysInternal(UAnimSequence* AnimationSequence, FName CurveName, const TArray<float>& Times, const TArray<DataType>& KeyData, ERawCurveTrackTypes CurveType, EInterpCurveMode InterpMode)
{
    checkf(Times.Num() == KeyData.Num(), TEXT("Not enough key data supplied"));

    const FName ContainerName = RetrieveContainerNameForCurve(AnimationSequence, CurveName);

    if (ContainerName != NAME_None)
    {
        // Retrieve smart name for curve
        const FSmartName CurveSmartName = RetrieveSmartNameForCurve(AnimationSequence, CurveName, ContainerName);

        // Retrieve the curve by name
        CurveClass* Curve = static_cast<CurveClass*>(AnimationSequence->RawCurveData.GetCurveData(CurveSmartName.UID, CurveType));
        if (Curve)
        {
            const int32 NumKeys = KeyData.Num();
            for (int32 KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
            {
                //Curve->UpdateOrAddKey(, );

                FKeyHandle handle= Curve->FloatCurve.UpdateOrAddKey(Times[KeyIndex], KeyData[KeyIndex]);

                FRichCurveKey& InKey = Curve->FloatCurve.GetKey(handle);

                InKey.InterpMode = RCIM_Linear;
                InKey.TangentWeightMode = RCTWM_WeightedNone;
                InKey.TangentMode = RCTM_Auto;

                if (InterpMode == CIM_Constant)
                {
                    InKey.InterpMode = RCIM_Constant;
                }
                else if (InterpMode == CIM_Linear)
                {
                    InKey.InterpMode = RCIM_Linear;
                }
                else
                {
                    InKey.InterpMode = RCIM_Cubic;

                    if (InterpMode == CIM_CurveAuto || InterpMode == CIM_CurveAutoClamped)
                    {
                        InKey.TangentMode = RCTM_Auto;
                    }
                    else if (InterpMode == CIM_CurveBreak)
                    {
                        InKey.TangentMode = RCTM_Break;
                    }
                    else if (InterpMode == CIM_CurveUser)
                    {
                        InKey.TangentMode = RCTM_User;
                    }
                }
            }
            AnimationSequence->BakeTrackCurvesToRawAnimation();
        }
    }
}