--- title: AdvancedLocomotionV4学习笔记(2)——AnimModifier date: 2022-08-24 09:39:43 excerpt: 摘要 tags: rating: ⭐ --- ## 前言 在AdvancedLocomotionV4中大量使用同步组、AnimNotify、AnimationCurve。但那么多动画Asset都要手动添加,我肯定是拒绝的。 ![](https://pic4.zhimg.com/80/v2-5fbfce38be263cf25a8e68cc0729228b_720w.jpg) 幸亏Ue4在4.16推出了AnimModifier功能以进行自动化操作。文档地址: [Animation Modifiers​docs.unrealengine.com/en-US/Engine/Animation/AnimModifiers/index.html](https://link.zhihu.com/?target=https%3A//docs.unrealengine.com/en-US/Engine/Animation/AnimModifiers/index.html) 本人有幸找到了一篇相关Blog: [http://www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/​www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/](https://link.zhihu.com/?target=http%3A//www.aclockworkberry.com/automated-foot-sync-markers-using-animation-modifiers-unreal-engine/) 并以此为基础编写了生成LocoMotion相关同步组、AnimNotify、AnimationCurve的AnimModifier(主要还是因为作者写的东西不太行),并且已经将其放入之前写的插件中,如觉得好用请给我点赞。 [https://github.com/blueroseslol/BRPlugins​github.com/blueroseslol/BRPlugins](https://link.zhihu.com/?target=https%3A//github.com/blueroseslol/BRPlugins) ![](https://pic2.zhimg.com/80/v2-20a332af4cb1b32284a7fa948ae5c0f9_720w.jpg) ## 使用效果与使用说明 因为图片上传有大小限制,所以我尽可能得压缩了图片,大家凑合得看看吧。 ![动图封面](https://pic2.zhimg.com/v2-cfb839fdc6490532d031545d134fef59_b.jpg) - 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生成的结果 VS AdvancedLocomotionV4原版曲线** ![](https://pic2.zhimg.com/80/v2-ad3035bb7113ea8ad84fc5be4d304b01_720w.jpg) 可以看得出 AdvancedLocomotionV4的同步组与AnimNotify略微往前。但我认为我的生成的才是正确的。 ## 具体实现说明 本人已经在蓝图中做了详细的注释,所以直接去看蓝图也是没问题的。下面将介绍具体实现方式: AnimModifier有2个需要实现的事件,分别是OnApply与OnRevert,指代了应用AnimModifier生成数据与撤销AnimModifier生成的数据。以下展示的是插件中OnApply事件的实现。(OnRevert只包含下图的3个节点) ![](https://pic3.zhimg.com/80/v2-857245a941d65fb6aaad5aa6d30cd0b2_720w.jpg) **ShouldApply** 通过判断设置的PathFilter来判断是否处理AnimSequence。 ![](https://pic2.zhimg.com/80/v2-0ed332a0c619f54b5cd670b10982d439_720w.jpg) **RemoveAll** 移除对应的Notify、Curve、Sync轨道。 ![](https://pic3.zhimg.com/80/v2-b282e2493ebcac67132993924c8e995e_720w.jpg) ## 创建所需轨道与取得所需数据 在进行完初始化后,按照设定的变量创建各种轨道: ![](https://pic4.zhimg.com/80/v2-ff1d357cb5f17c03953f8743b8df936b_720w.jpg) 按照设置的脚部骨骼进行循环,生成对应的曲线轨道,并且取得AnimSequence的时长,之后再对AnimSequence的每一帧进行逐帧处理。 ![](https://pic3.zhimg.com/80/v2-2d7c629243bd3d99ad5cc2865dcf47fe_720w.jpg) 通过GetBoneLocationRelativeToAtTime函数计算相对位移之后再根据的轴向取得偏移值(一般都是Z轴方向) ![](https://pic4.zhimg.com/80/v2-6dba54c66b9c7137040aba143b849d8b_720w.jpg) ## **GetBoneLocationRelativeToAtTime** 通过FindBonePathToRoot函数取得指定骨骼到根骨骼的骨骼链数组,之后将各个的骨骼的Translation值相乘,以得到指定骨骼与根骨骼的相对位移值。 ## 数据处理 因为之后需要对生成的曲线进行归一化处理,所以在红框处,我将所取得的每帧曲线数据进行存储。 白框处先是通过计算脚部骨骼的相对位移来判断脚是踩到地还是抬起来,之后再设定同步组。 ![](https://pic1.zhimg.com/80/v2-32788821fbf38b4dcd599f8de3f9f9e0_720w.jpg) 蓝框处则是在判断左右脚之后设置了对应的曲线值(Feet_Position)。 ![](https://pic1.zhimg.com/80/v2-50d5df93fea07280e467264bc3f7f49c_720w.jpg) 这里我重写了AddFloatCurvekeyWithType函数,这样就可以给关键帧指定插值方式了。具体代码我放在本文最后了。 ## 循环完每一帧之后 归一化曲线 ![](https://pic3.zhimg.com/80/v2-17c65f535cb89e5afe4c071ecbdaa74a_720w.jpg) 添加曲线(对应骨骼名称曲线),之后将数据清零。进行下一个骨骼的循环。 ![](https://pic3.zhimg.com/80/v2-691c830d589fc5b1488d21aaa066308a_720w.jpg) ## 最后处理 Feet_Position曲线的开头与结尾处没有关键帧,此时在进行判断后添加。 ![](https://pic4.zhimg.com/80/v2-ef30a0090925cfe664f8fd82807a5487_720w.jpg) 最后再执行FinalizeBoneAnimation。 ## AnimationBlueprintLibrary扩展 AnimModifier中的添加曲线函数AddFloatCurveKey与AddFloatCurveKeys没有提供修改曲线插值类型选项,所以我简单扩展了一下AnimationBlueprintLibrary,具体的可以参考我的插件代码。(懒得提交Pull Request了) ```cpp void UAnimBlueprintLibrary::AddFloatCurveKeysWithType(UAnimSequence* AnimationSequence, FName CurveName, const TArray& Times, const TArray& Values, EInterpCurveMode InterpMode) { if (AnimationSequence) { if (Times.Num() == Values.Num()) { AddCurveKeysInternal(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 void UAnimBlueprintLibrary::AddCurveKeysInternal(UAnimSequence* AnimationSequence, FName CurveName, const TArray& Times, const TArray& 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(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(); } } } ```