This commit is contained in:
2025-08-02 12:09:34 +08:00
commit e70b01cdca
2785 changed files with 575579 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
---
title: AdvancedLocomotionV4学习笔记1——动画节点
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
本人最近在学习AdvancedLocomotionV4并尝试将其中可用部分移植到自己的项目上。在此分享一些自己的心得。
## 学习建议
首先应该从动画蓝图ALS_AnimBP看起Editor为后处理动画蓝图可以跳过。个人建议从AnimatoinGraph开始看你可以学习到状态机的层次逻辑、合理的动画图表折叠模式、IK的实现、动画融合逻辑之后的文章我会举个例子说明
其中的相关变量驱动着状态机切换与动画融合所以下一步你需要看UpdateGraphAdvancedLocomotionV4将事件与更新逻辑分成了2个Graph之后是EventGraph。这里是动画蓝图与角色类的数据沟通与变量更新逻辑都在于此。完成以上两个步骤后你就了解了这个项目的总体脉络了但还是推荐先把熟悉一下Ue4相关的动画要点。
## Anim Modifiers
一种用于修改 Animation Sequence 或者 Skeleton Asset的蓝图类。
例如,精确定位左脚或右脚踩到地面时的帧来创建自动脚部同步标记(等诸如此类的各种应用)。通过使用该信息,可以将 动画同步标记 添加到脚部骨骼处于最低点(或接触地面)时的帧。
在AdvancedLocomotionV4中有大量的同步组、AnimNotify、以及曲线纯靠手动指定费事费力。所以本人尝试制作了相应的AnimModifier将在下一篇文章中分享。相比之下AdvancedLocomotionV4的Anim Modifiers做的就十分的菜了让人怀疑是不是作者在隐藏些什么。
## Virtual Bones
有了它就无需因为要制作Ik、FK等效果而将原始美术资源返工重做。不过4.24有个限制就是无法重复添加相同父骨骼与相同目标骨骼的虚拟骨骼。
注意LHS_ik_hand_gun用在后处理类型的动画蓝图Editor。
## Anim Curve
可以在SkeletonAsset编辑器中查看所有的Curve并且进行添加。
之后就可以在AnimationSequence中添加Anim Curve。但有个尴尬的问题就是无法查看哪些AnimationSequence设置了指定Curve。没有很好的引用关系图这让学习项目的人十分头疼。总不可能为了学习而去写一个编辑器插件吧……
## Pose Blend 与 Pose Driver
这个两个东西和流程相关,如果没有特殊流程,只适合于制作表情系统或是较为简单的动画。(有大佬告诉我还可以做肌肉效果,毕竟表情也是一种肌肉效果……)
## 节点
### FABRIK
俗话说的正向动力学,关节弯曲效果。
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/IKSetups/diagram_FK.webp)
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/IKSetups/diagramIK.webp)
### 转换空间节点
LocalSpace局部空间骨骼变换坐标系为其父骨骼所在坐标系。
ComponentSpace组件空间骨骼变换坐标系为SkeletalMeshComponent所在坐标系。
使用他们的原因在于,动画蓝图默认处于 局部空间。但特定的混合节点和所有的Skeletal Controls节点都处于组件空间中。所以在使用这些节点时需要进行空间转换。
注意空间转换会带来性能损耗所以建议把Skeletal Controls节点都放在一起进行计算。
### Skeletal Controls
#### AnimDynamics
这个节点是一个小型动力学解算器。可以给SkeletalMeshComponent提供一些次要的物理效果。可以用来给锁链、头发、披风加上简单物理效果。
需要注意:
- 惯性效果是通过把骨骼转换为Box来计算的
- 不计算碰撞,但计算在物理资源中设置的约束
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/AnimDynamics/AnimDynamicsChain.gif)
#### Apply a Percentage of Rotation
从指定骨骼获取旋转值,并按照指定比例应用到目标骨骼上。
#### Copy Bone
将指定骨骼数据复制到另一个骨骼上。将上面的模拟效果复制到另一侧的弹药包上。
#### Observe Bone
在界面中显示指定骨骼的Translate信息。
#### Spline IK
样条 Ik解算。用于模拟类似脊椎之类的样条结构骨骼动画效果。
#### Spring Controller
弹簧控制器,用于模拟因为前一个动作所造成的反作用力效果,就好像指定处的骨骼换成了弹簧。
#### Trail Controller
轨迹控制器,可以用于类似贪吃蛇之类动画效果。
#### Look At
让指定骨骼朝向目标骨骼位置。
#### Modify Curve
修改指定名称的曲线值在Animation Sequence中曲线值可以在材质、Niagara、蓝图中获得。
#### Rigid Body
使用在物理资源中设置碰撞体数据进行物理效果模拟。
在使用此节点之前,先在骨骼大纲视图选中所有需要模拟骨骼,点击右键将其设置为 Simulated 类型。
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/RigidBody/RigidBody_02.webp)
![image](https://docs.unrealengine.com/4.27/Images/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/RigidBody/RigidBody_11.webp)
#### Hand IK Retargeting
对应用了IK效果的骨骼进行重定向。按照给定参数向指定的FK骨骼进行偏移。
#### Two Bone IK
适用于两根骨骼的反向动力学解算节点。
#### CCDIK
4.21加入的节点。轻量级可用于驱动一段骨骼链。CCDIK提供了定义角约束的功能需要在解算中限制任意骨骼的旋转时较为实用。
#### Bone Driven Controller
通过其中一个骨骼信息来设置另一个骨骼的信息。
例如B.Localtion.x=A.Localtion.y;
https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/NodeReference/SkeletalControls/BoneDrivenController/
### 视频推荐
Cesar SC制作了UE4 Skeletal Controls系列视频里面有简单演示过程可以帮助你理解以上节点。
https://youtu.be/FrwB6iVh_DY
## PhysicalAsset
刚体类型有用于碰撞的、也有用于模拟的用来做肚子上的肥肉效果如果模拟中出现乱弹现象可以通过删除部分重叠刚体的碰撞来解决。4.18改善了编辑器,可以更好地观察刚体、约束、骨骼之间的关系。操作也方便了。具体的可以去看官方的视频。
## 布料与碰撞
通过在PhysicalAsset中创建胶囊来实现布料碰撞布料只支持胶囊体
注意我在这个过程中遇到了碰撞失效的问题后面发现是因为根骨骼的scale值不为1造成的动画外包在导出文件时候场景单位和导出单位设置有问题造成的。
### biped骨骼与rootmotion
Biped骨骼的根骨骼会强制定义为Biped001且无法修改。所以为了匹配Ue4的RootMotion动画规则。我们需要在在Biped001根节点外面再加一条Root骨骼。
经过测试其他商城中的RootMotion动画也可以正确地重定向Biped骨骼中。
### 布料与环境碰撞
想要与环境碰撞比就必须要StaticMesh的mobility 设置为Static。
在角色的SkeletalMesh中检查cloth-Collide with Environment是否勾选。并且关闭Collide with Attached Children选项。
## 其他有用功能
自动重新导入功能,在编辑器设置-General-Loading & Saving-Auto Reimport。动画师会十分喜欢这个功能因为在DCC保存动画数据后Ue4就会自动更新不用再去点ReImport了。

View File

@@ -0,0 +1,185 @@
---
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 Modifiersdocs.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/BRPluginsgithub.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_roffset为骨骼判断偏移值。
- AnimNotfiyClass:指定需要添加的AnimNotify。
- StepOnValue脚着地的判断高度。
- StepNextValue脚离地的判断值。一般是1~5
- InterpModeBonePosition曲线的插值模式。
**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<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();
}
}
}
```

View File

@@ -0,0 +1,142 @@
---
title: AdvancedLocomotionV4学习笔记3——思路整理
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
AdvancedLocomotionV4是一个不错的Ue4动画项目它的动画蓝图的层级结构与数据处理结构可以说是教科书级别不管是拿来学习还是直接拿来做项目都是不错的选择。本人通过将这个项目手动移植到自己的Demo中进行学习现写此文作为学习笔记并分享给打算学习该项目者。在大致了解这个项目的思路以后不但可以少走一些弯路提升学习速度而且可以将这个设计思想用在公司的其他项目中。
# 动画蓝图逻辑部分
首先应该从动画蓝图部分开始看起,它的位置在:
> CharacterAssets\MannequinSkeleton\ALS_AnimBP
动画蓝图分为**事件图表**与**动画图表**,这里先看**动画图表**部分。
## 动画图表
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBP_Source.png)
我认为这里的动画层级比较值得学习。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBP.png)
在这里我会简单介绍几个状态机以及层级关系至于虚拟骨骼与IK部分则会放在后面的文章中。
**针对流程不通用的动画**ALS_AnimBP只要针对地面上的运行动画Locomotion所以对于一些特殊动画需求比如攀爬系统。因为之前的骨骼控制流程与RagDoll不能共用所以还是适合制作一个子动画蓝图。至于是将子动画蓝图嵌入主动画蓝图中进行Pose融合还是在Character中直接对动画蓝图进行切换。就看大家选择了。
### 状态机与动画融合部分
AdvancedLocomotionV4采用了主Pose与细节动画叠加、多个状态机OutputPose融合的方式实现动画融合逻辑。
将动画拆分Pose与细节动画做法我能想到的好处有
1. 能更方便地调整动画融合结果参数化而且不用返回Dcc软件中调整
2. 可以通过动画节点对一些细节动画进行重用。
3. 增量式的修改,不会影响其他部分的融合结果。
当然这需要动画师掌握Maya里的动画层功能Max我不太清楚。想要了解我上述说的优点可以去看一下Locomotion Detail状态机。
AdvancedLocomotionV4里2个运动类型,站姿类型与蹲姿类型。也因此里面会有多个状态机。多个状态机OutputPose融合的方式会让一些刚学习的开发者遇到困难所以下面会对各个状态机进行大致介绍当你对每个状态机都有一个大概理解后看这个动画融合过程就轻松多了。
**BasePose**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BasePose.png)
基础Pose控制。切换站姿与蹲姿基础Pose。
**OverlayState**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/OverlayStates.png)
通过叠加的方式切换上半身的Pose。
**BaseLayer**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BaseLayer.png)
这里才是真正的主要动画融合。因为本人的Demo没有蹲姿状态所以蹲姿相关的融合逻辑就麻烦读者自己去研究了。
**MainMovementState**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MainMovementStates.png)
这个状态机就和Ue4第三人称模板有点像控制着几个优先级较高的状态着落、跳跃、空中与下落。在LandMovement与Grounded状态中会引用**MainGroundedStates**这个状态机。
**MainGroundedStates**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MainGroundedStates.png)
这里主要是翻滚、站姿势与蹲姿切换。因为本人的Demo不需要蹲姿翻滚会用Montage来做所以本人在自己的Demo把这个状态机去掉了。在Standing状态中会引用**(N) Locomotion States**状态机;在Crouching LF状态中会引用**(CLF) Locomotion States**状态机。
**(N)Lococmotion States**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterRotation.png)
基础运动状态主要是运动、停止运动以及站立时的角色旋转切换。其中在Moving与Stop状态中有引用**(N)Lococmotion Details**状态机。
**(N)Lococmotion Details**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/PivotContorl.png)
运动细节修饰AdvancedLocomotionV4在这里做了重心偏移效果。这里所有的状态都引用了**N)Lococmotion Cycles**状态机。
**(N)Lococmotion Cycles**
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LocomotionCycle.png)
运动循环动画4方向跑步与走路的动画融合其中左右各有2个前后切换用动画
### 状态机制作顺序与思考
本人介绍的顺序是有外到内,高层到基础。但如果实际制作项目,其顺序应该是相反的。当然你也可以在搭建完的**运动循环动画**部分后按照AdvancedLocomotionV4把这些结构先搭好创建对应的状态机并直接引用之后就需要考虑哪些是可以共用的状态与流程在最外层与BaseLayer中最后再进行细化。
在AdvancedLocomotionV4中展示了基础运动状态、运动细节修饰、运动循环这种经典三段论可以以此为基础进行开发之后再针对需求进行更多的细化。同时还展示了如何插入不同BasePose的动画蹲姿与站姿。我认为学习与熟悉这些思路才是这个项目的真正意义。
## 事件图表
AdvancedLocomotionV4将Tick事件单独剔除作为UpdateGraph。
另外事件图表中的内容大致为BlueprintInitializeAnimation、AnimNotify、蓝图接口实现与其他自定义事件。其中BlueprintInitializeAnimation的逻辑就只是用当前Pawn给Character变量赋值以方便后续从Character获取各种变量。
**UpdateGraph**:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBPUpdateAnimation.png)
- UpdateCharacterInfo:调用角色类的2个接口函数获取驱动动画用的数据。AnimBP只通过这连个接口函数获取所需的角色数据
- UpdateAimValues使用之前获取的数据计算Aim动画所需变量。包括头部与身体Aim类似黑魂的目标锁定
- UpdateLayerValue从动画Asset中获取对应的曲线值并更新对应变量。
- UpdateFootIk使用之前获取的数据计算脚部Ik效果所需变量。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterBPTick2.png)
下面就是根据不同的运动状态进行计算。
# 动画数据处理部分
动画所需的数据处理逻辑位于角色类中,它的位置在:
>Blueprints\CharacterLogic
- ALS_AnimMan_CharacterBP 一些更换SkeletonMesh之类的逻辑。
- ALS_Base_CharacterBP 动画数据处理与输入控制等其他基础逻辑。
- ALS_Player_Controller UI与摄像机CameraManager控制部分。
另外说明一下AdvancedLocomotionV4会通过表进行一些基础运动数值控制位置在
>AdvancedLocomotionV4\Data\DataTables\MovementModelTable
两个接口函数实现:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BPI1.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/BP2.png)
换句话说如果出现动画bug也只需要从这些变量开始找问题即可。这些变量都是tick函数中进行更新下面是tick
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CharacterBPTick.png)
因为结构比较清晰,而且看函数名就知道是干什么的关系,这里我就不详细介绍了。
# 虚拟骨骼
AdvancedLocomotionV4的虚拟骨骼都是用来制作Ik效果的除了VB Curves这个骨骼我也搞明白是干什么的。因为4.24版本无法在一个骨骼上创建多个虚拟骨骼这导致了非HumanIK骨骼无法套用AdvancedLocomotionV4的IK效果。所以套用这个模板时最好使用Ue4的骨骼结构。
# Animation Insights
另外推荐4.25新推出的Animation Insights。
https://zhuanlan.zhihu.com/p/147648443
目前感觉有点难用,没有开始与结束捕获的快捷键。也不知道支不支持记录使用自动测试框架的动画结果。
# 有关项目移植
首先需要按照蓝图中InputGraph的输入事件名在项目设置中添加对应的输入事件。
之后重新定义蓝图的父类在蓝图编辑器的File-ReparentBlueprint。至于动画蓝图则可以在对应的Asset上右键Retarget AnimationBlueprint-Duplicate AnimationBlueprint And Target。
PS.我前几天才知道蓝图有一个重新设置父类的功能,之前学习的时候都是手动一个一个复制的……

View File

@@ -0,0 +1,68 @@
---
title: AdvancedLocomotionV4学习笔记4——8方向运动
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
之前研究一AdvancedLocomotionV4的8方向运动就是旋转模式中的LookingDirection与Aiming方式。AdvancedLocomotionV4没有直接使用BlendSpace对8方向动作进行融合而是手动构建了自定义的融合方式本质上是将一个4方向运动动画融合成8方向。
PS4 的血源诅咒在8方向运动上做的不是很好如果尝试做出360度锁定运动就会硌脚的现象。其原因是没有前后运动切换过度动画。这里我们看一下AdvancedLocomotionV4的实现方式
# AnimGraph部分
## 前后运动切换
首先看状态机(N)Lococmotion Cycles
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LocomotionCycle.png)
中间有6个BlendSpace输入到Cycle Blendering动画层中而这些BlendSpace分别为对应方向的WalkPose、RunPose与Walk、Run的融合。自己制作的话记得给AnimSequence添加同步组
在Cycle Blendering动画层则会对输入的动画进行Cache Pose。并在(N)Direction State状态机中使用。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/MovementDirction.png)
上图为(N)Direction State状态机可以看出一共6个状态但其中LF、LB与RF、RB可以分别认为是分别将L与R拆分为前后两个部分。下图展示了LF与LB
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LF.gif)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/LB.gif)
其中F与B互相切换除了被MovementDirection枚举控制外还受到Feet_Crossing与HipOrientation_Bias曲线的控制。Feet_Crossing让角色不会在两脚交叉或是两脚前后重合时进行前后切换。HipOrientation_Bias曲线我并没有找到但从字面意思猜应该是当处于指定骨盆偏移方向时限制状态切换。
具体为:
在满足MovementDirection的情况下还需要满足
XF=>XB
```
GetCurveValue(HipOrientation_Bias)>0.5 && GetCurveValue(Feet_Crossing) ==0
=True
```
XB=>XF,条件有2。
```
|GetCurveValue(HipOrientation_Bias)| < 0.5 &&
StateWeiget(MoveLB)==1 &&
GetCurveValue(Feet_Crossing) ==0
=True
```
```
GetCurveValue(HipOrientation_Bias)<-0.5 && GetCurveValue(Feet_Crossing) ==0
=True
```
有了同步组以及切换条件之后前后切换就相当自然了。当然这自然也需要角色类中的进行Rotation的平滑计算。如果旋转过快还是会出现不自然的情况。
## 状态机对应的事件
这里的每个状态还绑定了对应的AnimNotify名称为AnimNotify_Hip XX系列。在EventGraph里可以看到这些AnimNotify用于设置TrackedHipsDirection的值。
# 对角动画融合
AdvancedLocomotionV4在这个状态机中使用了对角融合方式。以下是LF的融合图看得出它是通过VelocityBlend结构体将Velocity的x、y的正数与负数拆成4个部分对F、B、LF、FR4个动画进行融合。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/AnimBlend.png)
如果看过所有状态就会发现4个动画的关系是 当前状态方向、两个临近状态方向与对角方向。这么的做的目的应该是考虑切换至紧邻状态的同时兼顾反方向运动状态。
# AnimationBlueprint中的数据计算部分
主要的数据计算位于UpdateGraphe的UpdateRotationValues中。首先调用Calculate Movement Direction。之后还会计算LYaw、FYaw、BYaw、RYaw的值。这些值之后会被用来设置YawOffset曲线。你可以在(N)Direction State状态机的各个状态中找到看到对应的使用。
## Calculate Movement Direction
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CalculateMovementDirection.png)
首先判断RotationModeVelocityDirection一直为Forward其他模式则会调用CalculateQuadrant函数通过当前的MovementDirection、V当前Velocity的旋转值与AimingRotationContorlRotation的差值以及FR、FL、BR、BL-Threshold进行计算用于确定4个方向范围4个参数的默认值为70、-70、110、-110
-70~70为Forward70~110为Right-70~-110为Left剩下的为Back。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/CalculateQuadrant.png)

View File

@@ -0,0 +1,53 @@
---
title: AdvancedLocomotionV4学习笔记5——FootIK实现
date: 2022-08-24 09:39:43
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
AdvancedLocomotionV4在FootIK的实现中主要使用Transform Bone设置骨骼位置与虚拟骨骼最终使用Two Bone Ik完成效果设置。除此之外还使用曲线来控制IK与动画的过度。
# AnimGraph部分
因为本人的Demo里角色不需要拿枪所以Hand IK部分以及后处理动画蓝图中的Ik_Hand_Gun我就略过了。
动画图表中的逻辑主要集中在FootIk动画层中
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/AdvancedLocomotionV4/FootIk.png)
动画逻辑分为三个部分:
1. 首先计算出FootLock_L/R_Location/Rotation并将骨骼Ik_Foot_L/R变换到对应位置。使用FootLock_L/R_Alpha变量作为融合Alpha。该操作目的应该是先确定IK效果的大致坐标。Ik_Foot_L/R是虚拟骨骼 VB IK_Foot_L/R_Offset与VB IK_Knee_Target_L/R的父骨骼
2. 再计算出FootOffset_L/R_Location、FootOffset_L/R_Rotation将骨骼Ik_Foot_L/R**偏移**到对应位置。还计算了PelvisOffset偏移了骨盆骨骼。
3. 将膝盖处的虚拟骨骼VB Ik_Knee_Target_L与Ik_Knee_Target_R以当前骨骼坐标系偏移vec3(-20,-30,0)vec3(20,30,0)使用Enable_FootIK_L/R权限值控制融合Alpha。
4. 调用TwoBoneIk实现最终的FootIK效果。EffectorTarget为虚拟骨骼VB IK_Foot_L/R_Offset作为位置数据Joint Target为Ik_Knee_Target_L/R作为旋转数据。
# UpdateGraph
上述所用到的数据皆在动画蓝图的UpdateGraph中的Update Foot IK函数计算。
## Update Foot IK
首先调用SetFootLock更新Foot Lock L/R相关数据在当前动画的当前帧中IK骨骼的位移值与旋转值
之后计算IK骨骼的位移与旋转的偏移值。最后计算骨盆IK偏移。
### SetFootLock
1. 从指定的曲线中获取浮点值假定为A
2. 判断 A>=0.99 || A<CurrentFootLockAlpha,就用浮点值A更新CurrentFootLockAlpha(ref)。这样可以实现脚在踏到地面前都不会产生Ik效果但在收回去会有一定的IK与动画融合的效果
3. 如果 CurrentFootLockAlpha>=0.99,就从指定骨骼处获取Location与Rotation来设置CurrentFootLockLocation(ref)与CurrentFootLockRotation(ref)。
4. 如果 CurrentFootLockAlpha>0(有数值)调用SetFootLockOffset函数来更新CurrentFootLockLocation(ref)与CurrentFootLockRotation(ref)。这样做是为了保证在角色在任意移动时,脚部处于都有着正确的位置。
### SetFootLockOffset
1. 分别从角色类与角色移动类中获取当前旋转值与之前的旋转值之后计算出两者相差了多少RotationDifference
2. 获取当前帧与前一帧之间相对于模型的位移的距离。(这一步没完全懂,应该是获取距离之后再进行,世界坐标系=》组件坐标系吧?)
3. 计算偏移后的坐标值并更新LocalLocation(ref)。
4. 计算偏移后的旋转值并更新RotationLocation(ref)。
### SetFootOffsets这一步很关键
在这个函数中发射了射线。
0. 从EnableFootIKCurve曲线中获取浮点值来判断是否对Offset相关变量进行清零。
1. 通过ik_foot_l/r与root骨骼计算出脚底坐标再通过预设的范围变量确定射线检测范围。如果射线有Hit到地面则对ImpactPoint、ImpactNormal进行赋值并配合预设变量FootHeight计算出脚的CurrentTargetLocation最后通过ImpactNormal计算出旋转值并赋值给TargetRotationOffset。
2. 通过CurrentTargetLocation对CurrentLocationOffset进行插值计算从而实现IK的渐变效果。
3. 通过TargetRotationOffset对CurrentRotationOffset进行插值计算从而实现IK的渐变效果。
### SetPelvisIKOffset
0. 如果有IK效果处于激活状态则开始骨盆IK计算。
1. 比较FootOffset L/R 的Z值选择往-Z轴方向偏移多的赋值给PelvisTarget。
2. 比较PelvisTarget与PelvisOffset的Z值根据情况对PelvisOffset进行插值计算从而实现IK的渐变效果。

View File

@@ -0,0 +1,9 @@
---
title: 骨骼共用功能
date: 2022-09-04 19:52:38
excerpt: 摘要
tags:
rating: ⭐
---
Compatible Skeletons
https://docs.unrealengine.com/5.0/en-US/skeletons-in-unreal-engine/#compatibleskeletons

View File

@@ -0,0 +1,34 @@
---
title: Iphone面捕与UE Cloth资料
date: 2023-04-19 14:32:09
excerpt:
tags:
rating: ⭐
---
# Apple 54 BlendShape (相关教程)
0. 制作54 BlendShape 插件
1. https://blendermarket.com/products/faceit
2. https://item.taobao.com/item.htm?spm=a21n57.1.0.0.1bed523caQSJlv&id=712178484301&ns=1&abbucket=0#detail
1. 虚拟人角色与演员表情进行匹配
1. Unreal Engine Live Link Tutorialhttps://youtu.be/L8RckB9ZRJE
2. Facial Control Righttps://www.youtube.com/watch?v=Rus-eOEv7eo
3. Calibrate Metahuman and Live Link Face the right wayhttps://www.youtube.com/watch?v=4elhEGL6iYQ
4. Live Link Face updated with built in face calibration for Virtual Productionhttps://www.youtube.com/watch?v=TEzVqzwpgE4
5. UE4 | Live Link Face | Facial Calibration Setup https://www.youtube.com/watch?v=BuUZW3rqJxM
6. How to Clean and Moddify Livelink Face Mocap in Unreal Engine 5.1https://www.youtube.com/watch?v=dvgZ4yKc4AQ
2. 增加某一BS的权重。
# 布料Kawaii、SRP、Cloth
1. SPCR Joint Dynamics
1. https://github.com/SPARK-inc/SPCRJointDynamicsUE4
2. 国人做的教学视频https://www.bilibili.com/video/BV128411p7vt/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290
3. 国人做的教学视频https://www.bilibili.com/video/BV1mg411t7oT/?spm_id_from=333.788&vd_source=d47c0bb42f9c72fd7d74562185cee290
4. 案例关卡MyProject/Content/Example
2. KawaiiPhysics
1. https://github.com/pafuhana1213/KawaiiPhysics/
2. 英文文档建议看日文https://github.com/pafuhana1213/KawaiiPhysics/blob/master/README_en.md
3. 案例关卡Content/KawaiiPhysicsSample/KawaiiPhysicsSample
3. Chaos Cloth
1. https://docs.unrealengine.com/5.1/en-US/clothing-tool-in-unreal-engine---properties-reference/
2. ContentExample Cloth

View File

@@ -0,0 +1,12 @@
---
title: UE5 MotionMatching 教程
date: 2023-07-07 10:56:07
excerpt: 摘要
tags:
rating: ⭐
---
# 文档
MotionWarping 官方文档https://docs.unrealengine.com/5.2/en-US/motion-warping-in-unreal-engine/
# 油管视频
MotionMatchinghttps://www.youtube.com/watch?v=rLEWEQjTOb8

View File

@@ -0,0 +1,21 @@
---
title: BVH相关
date: 2024-01-07 12:33:20
excerpt:
tags:
rating: ⭐
---
# BVH库
- c++
- https://github.com/BartekkPL/bvh-parser
- Maya
- https://github.com/jhoolmans/mayaImporterBVH
```python
import bvh_importer
bvh_importer.BVHImporterDialog()
```
# Blender FBX => BVH
>因为Blender轴向与FBX不同。所以通过Blender导入FBX再导出BHV到Maya结果是错的。
**Blender需要导出YZX轴向BVH导入Maya后结果才正确。** 之后还需要手动将根骨骼改成root。

View File

@@ -0,0 +1,242 @@
---
title: Qt & FBX相关笔记
date: 2023-11-06 11:45:14
excerpt:
tags:
rating: ⭐
---
# FBX SDK setup for Qt
https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_welcome_to_the_fbx_sdk_html
使用之前建议**仔细查看文档**。FBX Sdk有三种库分别为动态链接库(dll + lib)、静态链接库(/MD)、静态链接库(/MT)。
- 动态链接库使用Qt的导入库功能导入动态库之后只需要再添加一行`DEFINES += FBXSDK_SHARED`即可。例如:
```c++
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
DEFINES += FBXSDK_SHARED
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
Common/Common.cpp \
main.cpp \
mainwindow.cpp
HEADERS += \
Common/Common.h \
mainwindow.h
FORMS += \
mainwindow.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/FBXSdk/lib/vs2022/x64/release/ -llibfbxsdk
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/FBXSdk/lib/vs2022/x64/debug/ -llibfbxsdk
INCLUDEPATH += $$PWD/FBXSdk/lib/vs2022/x64/debug
DEPENDPATH += $$PWD/FBXSdk/lib/vs2022/x64/debug
INCLUDEPATH += $$PWD/FBXSdk/include
```
- 静态链接库(/MD)使用Qt的导入库功能导入静态库之后
- 静态链接库(/MT)使用Qt的导入库功能导入静态库之后
## Qt相关设置添加
https://blog.csdn.net/libaineu2004/article/details/105718847
Qt在pro中设置运行时库MT、MTd、MD、MDd重点关注QMAKE_CFLAGS
多线程调试Dll (/MDd) 对应的是MD_DynamicDebug
多线程Dll (/MD) 对应的是MD_DynamicRelease
多线程(/MT) 对应的是MD_StaticRelease
多线程(/MTd)对应的是MD_StaticDebug
##  /NODEFAULTLIB:library
```cpp
win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../LIBRARYNAME/Lib/ -lLIBRARY /NODEFAULTLIB:library
```
## Ubuntu安装需要额外安装Qt
```cpp
sudo apt install --reinstall libgl-dev
```
新版本fbxsdk需要自己link xml2库所以需要手动安装一下
```bash
sudo apt-get install libxml2-dev
sudo apt-get install libxml2
```
之后再qt的pro中添加`LIBS += -lxml2`即可。
之后运行程序会提示# (error while loading shared libraries: libfbxsdk.so: cannot open shared object file: No)
这是因为Linux的动态链接库寻找方式与windows不同所致我们需要添加lib搜索路径
使用VSCode打开/etc/ld.so.conf 输入libfbxsdk.so的路径再运行ldconfig即可解决问题
```
1> vim /etc/ld.so.conf //在新的一行中加入库文件所在目录
2> /usr/lib //添加的目录路径
3> /usr/local/lib //添加的目录路径
3> ldconfig //更新/etc/ld.so.cache文件
```
参考
https://blog.csdn.net/sinat_14854721/article/details/111191139
另一种思路就是使用linuxdeployqt打成类似MAC的APP包
- https://github.com/probonopd/linuxdeployqt
- https://blog.csdn.net/zyhse/article/details/106381937
# FBX结构
![](https://help.autodesk.com/cloudhelp/2020/ENU/FBX-Developer-Help/images/scene_org.png)
FBX SDK场景图是通过`FbxScene`类抽象出来的。场景被组织为节点层次结构 ( `FbxNode`)。场景的根节点通过 访问`FbxScene::GetRootNode()`。场景元素(例如网格、灯光或相机)是通过将`FbxNode`与 的子类组合来定义的`FbxNodeAttribute`。
使用Ansii方式输出就可以使用vscode直接查看内部数据结构。
- GlobalMaterial世界坐标矩阵
- LocalMaterial相对空间矩阵
## 动画
- 动画堆栈 ( `FbxAnimStack`):动画数据最顶层容器,每一个**AnimStack**可以视为一个镜头,包含多个**FbxAnimLayer**。
- 动画层 ( `FbxAnimLayer`):每个**AnimLayer**中可以包含多个**FbxAnimCurve**。
- 动画曲线节点 ( `FbxAnimCurveNode`)
- 动画曲线 ( `FbxAnimCurve`) 动画以曲线的方式存在。可以针对骨骼的Location、Rotation分别添加曲线。
- 动画曲线关键点 ( `FbxAnimCurveKey`):动画关键帧。
每个动画数据都存在对应的节点的属性中Maya中可以给Shape的属性K帧可以通过类似`lAnimCurve = pNode->LclTranslation.GetCurve(pAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y);`的方式取得动画曲线。
UE导出的FBX相关属性为
添加动画关键帧的方式
```c++
FbxAnimCurve* lCurve = lRoot->LclRotation.GetCurve(lAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true);
if (lCurve)
{
lCurve->KeyModifyBegin();
lTime.SetSecondDouble(0.0);
//添加时间
lKeyIndex = lCurve->KeyAdd(lTime);
//添加数值与插值模式
lCurve->KeySetValue(lKeyIndex, 0.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(1.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, 45.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(2.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, -45.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lTime.SetSecondDouble(3.0);
lKeyIndex = lCurve->KeyAdd(lTime);
lCurve->KeySetValue(lKeyIndex, 0.0);
lCurve->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic);
lCurve->KeyModifyEnd();
}
```
### BindPose
遍历所有节点并对取得GlobalTransform矩阵最后添加到Pose里。
```c++
// Now create a bind pose with the link list
if (lClusteredFbxNodes.GetCount())
{
// A pose must be named. Arbitrarily use the name of the patch node.
FbxPose* lPose = FbxPose::Create(pScene,pPatch->GetName());
// default pose type is rest pose, so we need to set the type as bind pose
lPose->SetIsBindPose(true);
for (i=0; i<lClusteredFbxNodes.GetCount(); i++)
{
FbxNode* lKFbxNode = lClusteredFbxNodes.GetAt(i);
FbxMatrix lBindMatrix = lKFbxNode->EvaluateGlobalTransform();
lPose->Add(lKFbxNode, lBindMatrix);
}
// Add the pose to the scene
pScene->AddPose(lPose);
}
```
### ResetPose
```c++
void StoreRestPose(FbxScene* pScene, FbxNode* pSkeletonRoot)
{
// This example show an arbitrary rest pose assignment.
// This rest pose will set the bone rotation to the same value
// as time 1 second in the first stack of animation, but the
// position of the bone will be set elsewhere in the scene.
FbxString lNodeName;
FbxNode* lKFbxNode;
FbxMatrix lTransformMatrix;
FbxVector4 lT,lR,lS(1.0, 1.0, 1.0);
// Create the rest pose
FbxPose* lPose = FbxPose::Create(pScene,"A Bind Pose");
// Set the skeleton root node to the global position (10, 10, 10)
// and global rotation of 45deg along the Z axis.
lT.Set(10.0, 10.0, 10.0);
lR.Set( 0.0, 0.0, 45.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// 添加Root骨骼矩阵到Pose中
lKFbxNode = pSkeletonRoot;
lPose->Add(lKFbxNode, lTransformMatrix, false /*GlobalMatrix*/);
// Set the lLimbNode1 node to the local position of (0, 40, 0)
// and local rotation of -90deg along the Z axis. This show that
// you can mix local and global coordinates in a rest pose.
lT.Set(0.0, 40.0, 0.0);
lR.Set(0.0, 0.0, -90.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// 添加第二个骨骼节点到Pose中
lKFbxNode = lKFbxNode->GetChild(0);
lPose->Add(lKFbxNode, lTransformMatrix, true /*LocalMatrix*/);
// Set the lLimbNode2 node to the local position of (0, 40, 0)
// and local rotation of 45deg along the Z axis.
lT.Set(0.0, 40.0, 0.0);
lR.Set(0.0, 0.0, 45.0);
lTransformMatrix.SetTRS(lT, lR, lS);
// Add the skeleton second node to the pose
lKFbxNode = lKFbxNode->GetChild(0);
lNodeName = lKFbxNode->GetName();
lPose->Add(lKFbxNode, lTransformMatrix, true /*LocalMatrix*/);
// Now add the pose to the scene
pScene->AddPose(lPose);
}
```
## GlobalSettings
场景的轴系统、系统单位、环境照明和时间设置可以通过`FbxScene::GetGlobalSettings()`获取FbxGlobalSettings来进行定义。
## FBXStream & 共享内存
FBXStream案例:https://github.com/hamedsabri/FbxStream
之前考虑通过共享内存来实现文件传递以此来规避IO瓶颈这里可以考虑参考FBXStream。本质是通过文字流使得 文件 <=> string。
这样就可以通过共享内存进行数据传递了。
让张柏林试试对一个文件使用Java进行字符串流化。
文档地址:
- https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_cpp_ref_class_fbx_stream_html
- https://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_cpp_ref_class_fbx_importer_html

View File

@@ -0,0 +1,9 @@
---
title: UE5动态修改动画蓝图方案只有链接
date: 2024-02-18 11:00:58
excerpt:
tags:
rating: ⭐
---
- https://forums.unrealengine.com/t/dynamic-sub-anim-instance-node/75720
- https://docs.unrealengine.com/5.3/zh-CN/using-animation-blueprint-linking-in-unreal-engine/

View File

@@ -0,0 +1,163 @@
---
title: UE5动画重定向核心逻辑笔记
date: 2023-08-18 18:08:12
excerpt:
tags:
rating: ⭐
---
# 前言
最近研究过了一下UE的重定向逻辑所以写点笔记作为记录。IK部分没有去看
- FIKRetargetBatchOperation::RunRetarget()编辑器调用的重定向函数。主要是复制对应需要重定向资产并且进行重定向之后通知UI。
- FIKRetargetBatchOperation::RetargetAssets():重定向资产逻辑。
- 复制所有曲线轨道(不含数值)。
- 设置Skeleton与PreviewMesh资产。
- 提前调用重定向资产的PostEditChange(),以避免编辑后钩子函数被调用,产生依赖顺序问题。
- ConvertAnimation()
- 替换动画蓝图并且编译。
# ConvertAnimation()
一开始是一些初始化逻辑主要的是调用UIKRetargetProcessor的Initialize()。里面初始化了
- SourceSkeleton & TargetSkeleton用于重定向计算用的数据载体存储了骨骼链、骨骼、当前重定向Pose等数据。
- RootRetargeter根骨骼重定向器。
- ChainPairsIK、ChainPairsFK用于FK与IK的骨骼链数据。
- UIKRigProcessorIK重定向处理器。
- 所有FIKRigGoal
从FRetargetSkeleton& SourceSkeleton & FTargetSkeleton& TargetSkeleton获取对应参数来进行之后的动画资产重定向循环循环逻辑如下
- 获取动画帧数,并且重新构建骨骼动画数据轨道。
- 取得速度曲线。
- 之后对每一帧Pose进行重定向主要是的逻辑是
- 取得当前帧Pose并将其转化WorldSpace。
- UIKRetargetProcessor Processor->RunRetargeter()
- 将重定向完的结果转化成LocalSpace并且将每个骨骼的数据放入对应骨骼动画数据轨道。
- 调用IAnimationDataController的AddBoneTrack() & SetBoneTrackKeys()给资产设置上关键帧。
# RunRetargeter()
此为核心重定向逻辑分为RunRootRetarget()、RunFKRetarget()、RunIKRetarget()、RunPoleVectorMatching()。
重定向数据会直接修改TargetSkeleton的Pose数据。Root与FK重定向执行完之后会调用UpdateGlobalTransformsBelowBone()更新后续骨骼的WorldSpace Transform。
## RunRootRetarget()
FRootRetargeter::EncodePose()
取得输入的根骨骼Transform数据并给`FRootSource Source`赋值。
FRootRetargeter::DecodePose()
```c++
FVector Position;
{
// 关键InitialTransform 为重定向Pose计算出的数值通过比值计算出Target的Current数值
const FVector RetargetedPosition = Source.CurrentPositionNormalized * Target.InitialHeight;
// 根据RetargetSetting中设置的BlendToSourceWeights与BlendToSource与SourcePosition进行混合。
Position = FMath::Lerp(RetargetedPosition, Source.CurrentPosition, Settings.BlendToSource*Settings.BlendToSourceWeights);
// 应用vertical / horizontal的缩放
FVector ScaledRetargetedPosition = Position;
ScaledRetargetedPosition.Z *= Settings.ScaleVertical;
const FVector HorizontalOffset = (ScaledRetargetedPosition - Target.InitialPosition) * FVector(Settings.ScaleHorizontal, Settings.ScaleHorizontal, 1.0f);
Position = Target.InitialPosition + HorizontalOffset;
// 应用RetargetSetting中Position偏移。
Position += Settings.TranslationOffset;
// blend with alpha
Position = FMath::Lerp(Target.InitialPosition, Position, Settings.TranslationAlpha);
// 记录偏差
Target.RootTranslationDelta = Position - RetargetedPosition;
}
FQuat Rotation;
{
// 计算Source的旋转偏差值
const FQuat RotationDelta = Source.CurrentRotation * Source.InitialRotation.Inverse();
// 将偏移加到Target上
const FQuat RetargetedRotation = RotationDelta * Target.InitialRotation;
// 将RetargetSetting上的旋转偏差值加上。
Rotation = RetargetedRotation * Settings.RotationOffset.Quaternion();
Rotation = FQuat::FastLerp(Target.InitialRotation, Rotation, Settings.RotationAlpha);
Rotation.Normalize();
// 记录Target的旋转偏差值
Target.RootRotationDelta = RetargetedRotation * Target.InitialRotation.Inverse();
}
// 将数据应用到根骨骼上
FTransform& TargetRootTransform = OutTargetGlobalPose[Target.BoneIndex];
TargetRootTransform.SetTranslation(Position);
TargetRootTransform.SetRotation(Rotation);
```
## RunFKRetarget()
遍历所有骨骼链并执行FChainEncoderFK::EncodePose()与FChainDecoderFK::DecodePose()。
FChainEncoderFK::EncodePose()
1. 遍历所有骨骼复制SourceGlobalPose到**CurrentGlobalTransforms**
2. Resize CurrentLocalTransforms
3. FillTransformsWithLocalSpaceOfChain() 转换成LocalSpace数据。
1. 遍历所有骨骼链取得对应骨骼的Index以及父骨骼Index。跳过根骨骼
2. 计算相对Transform后放入对应ChainIndex的OutLocalTransforms中。
4. 根据ChainParentBoneIndex将父骨骼Transform赋予给ChainParentCurrentGlobalTransform。
FChainDecoderFK::DecodePose()
1. 更新IntermediateParentIndices的所有Index的父骨骼WorldSpace Transform。用来更新重定向后哪些不属于骨骼链的骨骼的WorldSpace Transform。在InitializeBoneChainPairs() -> InitializeIntermediateParentIndices()中初始化)
2. 从根骨骼开始重定向以保证最后结果不会发生偏差。计算了Source/Target InitialDelta、Target 根骨骼 Transform后将两者相乘最后应用给所有Source骨骼链。
3. 如果禁用FK重定向则返回当前WorldSpace Transform并且return。
4. 计算Source & Target 骨骼链开始Index。
5. 开始遍历所有骨骼。
1. 取得Target骨骼Index以及InitialTransform。
2. 根据设置的FK的旋转模式执行对应逻辑默认为插值模式也就是他实现了骨骼链的不同数量骨骼间的重定向匹配。计算SourceCurrentTransform与SourceInitialTransform。
1. 使用Target的Params骨骼长度百分比来插值计算出Source的Transform。参考[[#插值计算]]。
2. Params的计算在FChainFK::CalculateBoneParameters(),可以看得出就是根据**当前骨骼长度/总长度**计算百分比最后加入Params。该函数的调用堆栈为UIKRetargetProcessor::Initialize() => UIKRetargetProcessor::InitializeBoneChainPairs() => FRetargetChainPairFK::Initialize() => FChainFK::Initialize()
3. 计算旋转值SourceCurrentRotation、SourceInitialRotation => RotationDelta TargetInitialRotation => **OutRotation**。
4. 计算当前TargetBone的父骨骼 ParentGlobalTransform。
5. 根绝设置的FK TranslationMode计算出 **OutPosition**。
6. 计算OutScale = SourceCurrentScale + (TargetInitialScale - SourceInitialScale),计算出**OutScale**。
7. 对应index的`CurrentGlobalTransforms[ChainIndex]`/`InOutGlobalPose[BoneIndex]`赋值。
6. 进行一些后处理计算,默认状态下跳过。
## 插值计算
```c++
FTransform FChainDecoderFK::GetTransformAtParam(
const TArray<FTransform>& Transforms,
const TArray<float>& InParams,
const float& Param) const
{
if (InParams.Num() == 1)
{
return Transforms[0];
}
if (Param < KINDA_SMALL_NUMBER)
{
return Transforms[0];
}
if (Param > 1.0f - KINDA_SMALL_NUMBER)
{
return Transforms.Last();
}
for (int32 ChainIndex=1; ChainIndex<InParams.Num(); ++ChainIndex)
{
const float CurrentParam = InParams[ChainIndex];
if (CurrentParam <= Param)
{
continue;
}
//关键在这
const float PrevParam = InParams[ChainIndex-1];
const float PercentBetweenParams = (Param - PrevParam) / (CurrentParam - PrevParam);
const FTransform& Prev = Transforms[ChainIndex-1];
const FTransform& Next = Transforms[ChainIndex];
const FVector Position = FMath::Lerp(Prev.GetTranslation(), Next.GetTranslation(), PercentBetweenParams);
const FQuat Rotation = FQuat::FastLerp(Prev.GetRotation(), Next.GetRotation(), PercentBetweenParams).GetNormalized();
const FVector Scale = FMath::Lerp(Prev.GetScale3D(), Next.GetScale3D(), PercentBetweenParams);
return FTransform(Rotation,Position, Scale);
}
checkNoEntry();
return FTransform::Identity;
}
```

View File

@@ -0,0 +1,376 @@
---
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;
}
```

View File

@@ -0,0 +1,39 @@
## 文档与视频
https://docs.unrealengine.com/zh-CN/Platforms/AR/HandheldAR/FaceARSample/index.html
https://www.youtube.com/watch?v=AIHoDo7Y4_g
## 前提条件
1. 这个APP没有上架APPStore现在已经上架了所以需要大家自己下载FaceARSample工程并打包。因为你至少需要拥有一台MAC想生成IPA文件则需要购买开发者证书。
2. 需要在光线充足的环境中录制。
3. 对应的角色的面部需要有对应的51个morph具体的可以参考文档。
4. 你必须有一台iPhone X以上的iphone。
## 大致步骤
1. 第一次运行需要打开菜单点击Calibration Mode进行校准。眼睛、帽子会影响校准
2. 如果使用LiveLink将数据传回PC那么就需要在之前点击FaceTracingMap2中的角色再点击Calibrate In Editor进行校准。
3. 使用Sequencer提供的录制功能进行录制。录制结果为Animation Sequence。之后就可以导出FBX了。
### 安装APP
如果你没有MAC或者不想花100刀购买开发者证书你可以选择使用低版本IOS的iphone(大致是2019年9月前的版本)再配合Cydia Impactor工具安装别人打包的IPA需要7天重新安装一次。当然也可以选择越狱的iphone。
本人因为手机已经升级过系统了,所以无法进行下一步测试。
## 其他
本人还查到了其他解决方案,隧在此分享。
### FaceCap与AdvancedSkeleton
FaceCap是AppStore上一款收费的APP可以免费录制3s动画进行测试。使用FaceCap录制面部动画从手机中将生成的fbx文件拷贝的电脑上。之后再通过AdvancedSkeleton插件映射到绑定角色上。
具体操作可以查看AdvancedSkeleton出品的教程
https://www.youtube.com/watch?v=ouf8jDMsXwE
但是这个流程有个问题就是文件需要拷来拷取十分不便。并且App无法像官方工程那样进行定制以提高制作效率。
### FX-Facial-Flow
于是另一个问题就产生了制作的51个morph。
我之前一直以为制作morph只需要让动画师移动一下模型的顶点就可以搞定了。但在和一个动画师朋友沟通发现这个过程需要一个专业模型师与动画师通力合作才能完成。同时制作的morph无法直接在其他模型上使用。
在这里他推荐了FX-Facial-Flow我也大致看了一下说明。应该是使用opencv以及一些深度学习技术。感觉可以提高制作效率。
因为价格不贵,同时还附带教程,所以推荐给大家(本人没有用过):
https://www.aboutcg.org/courseDetails/666/introduce

View File

@@ -0,0 +1,145 @@
---
title: 动画蓝图修型相关
date: 2023-01-12 17:03:24
excerpt:
tags:
rating: ⭐
---
## DAWA数字人
主资产(全裸身体模型)使用`dawa_Skeleton_AnimBlueprint`动画蓝图后处理蓝图用于在编辑器中预览大部分曲线用于控制材质效果EPIC的数字人技术其他衣服使用CopyPose将主资产的动画结果拷贝到模型的骨骼上。
PS.
1. 调试时需要将后处理动画蓝图清除。
2. 动画蓝图的tick部分那么多节点完全可以使用c++以提高性能。
### 流程
1. 表情使用苹果ARKit驱动表情。此外LiveLink Face 也可以用于驱动一些曲线来控制头部旋转但需要使用EvaluateLiveLinkFrame节点。本项目没有使用。
2. AnimLayer biaoqing主要用于计算与更新曲线数值来实现一些材质/(Morph Target/Blend Shape)修型效果。该项目的动画资产并没有设置曲线值,所以这个层大部分逻辑都在空跑。
3. Kawaii节点用于制作乳摇。
4. SpringController 系列节点应该是没用作用的。
5. AnimLayer xiuxing一堆PoseDriver节点与2个上臂骨骼偏移。主要是通过PoseDriver进行修型。
6. AnimLayer bonedriver使用骨骼的一些数值对骨骼数值进行修改。
7. AnimLayer Shoes根据高跟鞋的形状调整脚部骨骼形变。该项目对脚部骨骼调整了旋转。
## Pose Driver
UE使用**Pose Driver**动画节点来实现修型,主要是通过几种类型的数据来驱动**Pose Targets**Morph Target/Blend Shape或者Curves。其主要参数是
- **源骨骼Source Bone** :由哪个骨骼进行驱动?
- **驱动源Drive Source**:根据骨骼的哪个数据进行数据插值?有**Rotation/Location**。
- **仅驱动骨骼Only Drive Bones**Pose Driver节点仅修改阵列中指定的骨骼Source Bone对应的修型Twist骨骼
1. 相当于过滤器相关的形变数据K在动画中位于PoseAsset。
- **姿势目标Pose Targets**指定的用于驱动的Pose集。
1. 着重需要设置好每个Pose的立面角。注意避免万向锁的产生。
2. 每个立面角最好保证有较小的重叠,以保证插值的正确性。
- **驱动输出Drive Output**:选择驱动输出的是**Curves/Poses**。
- **RBFParams**:插值参数。
### 小灰人的修型方案
主要分为**Arm_Left/Right**与**Leg_Left/Right**。修型的方案基本查看骨骼与MorphTarget的名字就可以看出来。
- Arm
- Clavicle
- clavicle_l/r_fwd_30
- clavicle_l/r_down_20
- clavicle_l/r_back_30
- clavicle_l/r_up_40
- Upperarm
- upperarm_l/r_back_45
- upperarm_l/r_out_55
- upperarm_l/r_out110_twist_in_70
- lowerarm_l/r_in_35
- upperarm_l/r_out_85
- upperarm_l/r_fwd_110
- upperarm_l/r_in_10
- upperarm_l/r_out110_twist_out_70
- upperarm_l/r_out_twist_a
- upperarm_l/r_fwd_15
- upperarm_l/r_out_15
- upperarm_l/r_back_10
- upperarm_l/r_out110
- upperarm_l/r_fwd_twist_in_a
- upperarm_l/r_fwd_90
- Lowerarm
- lowerarm_l/r_in_110
- lowerarm_l/r_out_35
- lowerarm_l/r_in_50
- lowerarm_l/r_in_75
- lowerarm_l/r_out_10
- lowerarm_l/r_in_90
- lowerarm_l/r_in_10
- Hand
- hand_l/r_down_90
- hand_l/r_up_20
- hand_l/r_up_90
- Leg
- Calf
- calf_l/r_back_50
- calf_l/r_back_120
- calf_l/r_back_90
- calf_l/r_back_150
- Thigh
- thigh_l/r_bck_10
- thigh_l/r_fwd_10
- thigh_l/r_in_45_out_90
- thigh_l/r_fwd_90
- thigh_l/r_bck_90
- thigh_l/r_bck_50
- thigh_l/r_out_110
- thigh_l/r_out_10
- thigh_l/r_fwd_45
- thigh_l/r_in_50
- thigh_l/r_fwd_110
- thigh_l/r_out_55
- thigh_l/r_out_85
- Foot
- foot_l/r_down_60
- foot_l/r_up_35
### 其他
#### Maya中的类似操作
在Maya中通过**Pose Editor**可以通过指定Pose+Morph实现动捕修型。
#### 使用Pose曲线驱动的程序动画
UE曲线驱动的动画https://docs.unrealengine.com/4.27/zh-CN/AnimatingObjects/SkeletalMeshAnimation/AnimHowTo/CurveDrivenAnimation/
1. 通过AnimSequence资产创建PoseAsset。
2. 使用PoseAsset->CreateAsset->CreateAnimation->ReferencePose创建一个只有参考Pose的空AnimSequence资产。
3. 通过添加Variables Curve对指定的Pose添加曲线来制作曲线驱动动画。
## 厄风女神Echo
角色蓝图没什么东西主要的逻辑位于Echo_AnimBP
- Echo_AnimLayerInterface被Echo_AnimBP引用。但里面没有东西。
- Echo_LiveLink_AnimBP演示基础的LinkLive用法。
后处理动画蓝图使用了Echo_PostProcess_AnimBP。
### Echo_AnimBP
简化简洁的AdvancedLocomotionV4逻辑。动画蓝图层级为
- Locomotion
- MainState
- FullyBodyLayer(Echo_AnimLayerInterface)
- AnimMontageSlot
- Inertialization过滤曲线
- ControlRig
### Echo_PostProcess_AnimBP
1. 将C Biped开头的修型曲线归零。
2. 使用PoseDriver进行修型。
1. neck_02
2. upperarm_l
3. lowerarm_l
4. calf_l
5. hand_l
6. upperarm_r
7. lowerarm_r
8. calf_r
9. hand_r
## 表情
- https://zhuanlan.zhihu.com/p/113396468
- https://arkit-face-blendshapes.com/
# UE5.3新添加的功能 PoseDriverConnect
- https://www.unrealengine.com/marketplace/zh-CN/product/pose-driver-connect
- https://www.unrealengine.com/zh-CN/blog/create-more-realistic-animation-in-less-time-with-pose-driver-connect
主要的功能是通过PoseDriver来驱动其他次级PoseDriver来此来得到更加好的修形效果。

View File

@@ -0,0 +1,96 @@
---
title: 外包过程中的动画重定向以及蒙皮调整经验
date: 2022-08-24 09:52:19
excerpt:
tags:
rating: ⭐
---
## 前言
本人最近给自己的Demo外包了若干角色动画也遇到了一些坑。所以这里给大家分享一下相关经验避免大家再次采坑尤其是那些缺乏经验、精力的独立游戏制作者。当然本人对3DMax完全不熟悉大部分信息都是本人与外包大哥沟通过程中产生的个人认知如有错误还请指正谢谢。
PS.本人因为布料碰撞问题导致重新导入6次角色包括绘制布料、设置碰撞与编写动画蓝图。真是太痛苦了
## 如果角色需要使用Ue4的布料需要额外确认的事情
1. 使用的是否是3DMax。
2. 场景单位问题:角色大小是否正常。
3. 骨骼问题根骨骼的缩放是否为1、旋转是否为0与布料碰撞有关骨盆骨骼也需要检查一下防止因为fbx导出轴向问题导致骨盆骨骼旋转。换句话说就是在拿到绑定后的角色时需要马上使用Ue4进行角色布料碰撞测试。
当然蒙皮与动画肯定是要检查的,一般靠谱的外包都不会犯这些错,所以这里我也不说了(主要是我也不太懂)。这里主要是我个人认为需要注意的问题:
第一点和第二点可以合起来说:因为3DMax的默认单位是英寸在绑定时有可能会出现因为导入模型时的单位不正确而导致角色缩小问题。至少本人遇到了这个问题整整郁闷了1个月。个人认为关键还是需要搞清楚max或者fbx文件的单位、max的系统单位以及输出单位。调整单位的方法有以下3个
## 单位问题
**设置Max场景单位**
Max导出文件单位不正确的主要原因在于文件本身的单位不正确。Ue4的默认单位为厘米而Max默认为英寸。所以我们可以通过调整单位来解决这个问题大致步骤如下 1. Cutomize-UnitsSetup进度单位设置命令。 2. 选择SystemUnitSetup。 3. 将单位设置为厘米。 4. 此时打开单位为英寸的max文件就会提示Units Mismatch,选择Rescale The File Objects To System Unit Scale。 5. 之后导出Fbx注意将导出单位改成厘米。
**场景缩放**
对于蒙皮文件,这个操作可能会产生问题。操作步骤如下: 1. 在Utilities栏中找到RescaleWorldUnits并打开。 2. 设置缩放因子后,点击确定。
**UnrealEngine导入设置**
在Ue4导入选项中有个单位缩放你可以直接在这里设置导入缩放比例。在没有Max源文件只有fbx文件的情况下只能选择这种方法。
> 注意如果你的角色要使用布料系统那么以上方法都会让布料的碰撞失效。经过试验如果根骨骼的缩放不为1以及旋转不为0就会出现碰撞失效的情况。因为这些方法本质就是缩放根骨骼。
一种简单的解决方法就是直接设置角色类骨骼模型的缩放。不过本人因为强迫症所以不推荐这种方法。具体的解决方法请参照**Maya蒙皮传递**这一节。这里我先说一下2个软件的骨骼系统之后再细说解决方法。
## 骨骼类型
据我所知3DMax有CS(biped)与CAT骨骼系统虽然它也有普通的骨骼但是因为角色动画库基本都是以这些骨骼系统为基础所以基本不会使用。
我找的外包大哥用的是CS(biped)。CS有点不爽的地方就是会都出一个Biped节点。
至于CAT结构和Ue4的HumanIK结构相似但外包大哥说这个不太稳定所以没有使用。
对应到Maya则是HumanIK工具结构和Ue4的HumanIK结构相似多出一节脚趾骨当然我目前没有和专门外包Maya动画的公司沟通过不太确定他们是不是使用HumanIK来做角色动画库的。如果公司打算专门给Ue4做动画可以考虑使用Ue4的动画插件。
这些骨骼系统对于动画其实并没有影响但如果你是一个和我一样的拿着自己的工资做Demo的傻逼个人开发者**因为资金有限无法外包所有的动画或是需要使用商场资源制作原型亦或是想套用AdvancedLocomotionSystemV的动画系统。那么我推荐你使用Ue4的标准骨骼进行绑定并使用重定向系统将其他骨骼系统的动画重定向到标准骨骼上。**
理论上动画师还是会使用动画库对应的骨骼系统再绑定一个角色,因为动画需要根据角色模型进行调整。当然绑定的精度不用那么高了。
## 动画重定向
因为本人使用的是Maya所以这里以Maya为主。首先需要制作用于重定向的标准的TPose就是把角色摆成一个十字架可以参考HumanIK工具的标准骨骼。其原因是因为TPose容易统一方便对齐。重定向Pose将会直接影响重定向的质量。所以我推荐在Maya中制作另一个原因在于Ue4的重定向工具无法根据现有骨骼动画处理IK骨骼动画。下面我将简单介绍Ue4与Maya中的重定向流程
**UE4**在Maya中制作完TPose后给骨骼K一帧动画并导出。在Ue4中导入后在得到的AnimationSequence上右键Create-Create PoseAsset之后就可以在RetargetManager左下的ManageRetargetBasePose界面中点击ModifyPose指定创建的TPose。
根据重定向源与目标不同还可以分为这里的非UE4骨骼动画以CS骨骼动画为准
CS=》Mannequin与Mannequin=》CS按照对应骨骼结构在RetargetManager的SetUpRig中选择HumanIK模板并设置对应骨骼即可。如果两个绑定角色的身高相同root骨骼可以置空。如果不同则需要在给CS骨骼再添加一根root骨骼。重定向过程中可能会出现手臂、腿部不正确的问题这可能是twist骨骼位移的问题需要将部分twist骨骼需要置空。
CS=》CS这种情况可能涉及到其他非标准骨骼所以这里需要在重定向源骨骼Asset上右键Create-Create Rig再按照对应骨骼结构填入。
**Maya**选择Rigging模式点击Skeleton-HumanIK运行。之后再点击CreateCharacterDefinition创建角色定义就可以指定对应骨骼了。
你可以手动指定也可以套用官方模板前提是你的骨骼没有改过名称模板有CS、CAT与HIK。大致步骤如下
[https://www.youtube.com/watch?v=ms1wGBjykA8www.youtube.com/watch?v=ms1wGBjykA8](https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3Dms1wGBjykA8)
如果你的骨骼改过名称了就需要手动指定了。如果你的角色骨骼没有处于TPose状态HumanIK界面中的大臂与手会显示为黄色所以遇到这种情况就需要调整当前骨骼为TPose状态。
在设置完重定向源与目标角色后:
1. 点击CreateControlRig添加控制之后角色会被锁住如果要再次修改则需点击边上的锁按钮。
2. 导入重定向源的动画。
3. 在HumanIK的Character中选择目标角色Source选择为重定向源角色此时重定向源的动画数据就已经重定向目标角色上了。
4. 如果你的目标角色为UE4标准骨骼此时就需要将IK骨骼P设置父子关系到对应的控制器上。 应该是将IK骨骼和对应的骨骼建立父子约束先选爹再选儿子这一步应该在设置TPose之前进行。
5. 选中骨骼、模型与控制器,并且导出动画。(记住需要点击烘焙动画选项)
## Maya蒙皮传递
大致操作如下:
1. 复制原有蒙皮文件作为新的蒙皮文件。在删除历史、断开连接后,调整骨骼与模型大小,并且冻结缩放与旋转属性。
2. 将原有蒙皮文件的骨骼进行批量重命名。
3. 将原有蒙皮文件导入新的蒙皮文件中。因为之后需要将模型合并所以需要确定UV通道是否正确如果你像我那样扣了UE4角色的眼睛的话
4. 将原有模型与调整过大小的模型分别进行合并之后切换到Rigging模型运行skin-BakeDeformationToSkinWeights。
5. 在大纲视图中选中对应项按住鼠标中键拖拽到对应栏中以完成指定。指定完选项点击Apply。再等一把王者顺风局的时间蒙皮的就传递好了。
## Maya批量命名脚本
如果你的重定向源骨骼名称与目标相同这就需要进行批量改名了。这是网上找来的脚本对于骨骼需要按住Shift并且点击+,展开所有骨骼节点,再选中批量添加后缀。
```text
from maya.cmds import *
sel = ls(sl=True)
numSel = len(sel)
for i in range(0,numSel,1):
origName = sel[i]
#print origName
rename(origName,origName+'_old')
```
## 结语
本文写的比较仓促如有不明白的地方欢迎交流。Maya中的重定向工作可以通过Python编写插件来实现批量重定向动画本人将会在Demo完成后尝试编写这个插件。

View File

@@ -0,0 +1,79 @@
---
title: 实时重定向与骨骼动画共用相关
date: 2022-09-25 21:20:07
excerpt:
tags:
rating: ⭐
---
## IK Rig
本质上是分离了以前版本动画蓝图中的IK解算逻辑。
大致步骤为:
1. 创建IK Goal与Solver。
2. 选中IK Goal并在想要的解算器上右键Connect Goal To Selected Solver来指定Solver的目标。
3. 选中Solver并且在想要的骨骼上右键Set Root Bone On Select ed Solver来指定Solver的根骨骼目标。
### IK Goal
意为IK目标。
### Solver
解算器类型主要为:
- Body Mover**躯体运动解算器**。需要一个根骨骼和至少两个IK目标相连。主要用于地面对齐应作为第一个解算器与其他解算器配对使用Limb IK或者Full Body IK。
- ![BodyMover4.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/BodyMover4.gif)
- Limb IK**手足/肢体 IK**。需要一个根骨骼和一个IK目标才能起作用。该解算器要求链中有**至少三根骨骼**才能正确工作。
- ![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230118103643.png)
- Full Body IK**全身IK**是个功能齐全的多IK Goal解算器。必须连接一个根骨骼和至少一个IK目标。
- ![FBIK1.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/FBIK1.gif)
- 可以对指定的骨骼设置FullBody设置来限制一些骨骼的旋转/位移。
- ![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121162327.png)
- Pole Solver**极解算器**,为单个骨骼链提供 **极向量Pole Vector** 控制。必须同时指定根骨骼和末端骨骼,右键点击根骨骼和末端骨骼并选择 **在选定解算器上设置根/末端骨骼Set Root / End Bone on Selected Solver**。在大多数情况中,应将"极解算器Pole Solver"与另一个解算器配对,并将"极解算器Pole Solver"设置为最后执行。
- ![Pole5.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Pole5.gif)
- Set Transform通常情况下最好将该解算器与其他解算器配对并设置为先执行"设置变换Set Transform"。在这个示例中,臀部的 **设置变换Set Transform** 与腿上的 **手足IKLimb IK** 进行了配对。
- ![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Transform2.gif)
解算器的添加顺序很重要因为解算器将根据名称旁边显示的数字按顺序执行。对于此IK系统肢体IK应该最后求值。
![Tut2.gif](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/Tut2.gif)
### IK Rig In Animation Blueprint
在为一个SkeletalMesh创建IK Rig之后还可以动画蓝图中进行控制。只需要使用IK Rig动画蓝图节点并且指定**Rig Definition Asset**。并在**IK Rig Editor**中将对应的**IK Goal**设置**Exposure Position/Rotation**。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230119102819.png)
## IK Rig Retarget
IK Rig的另一个作用就是IK Rig Retarget可以使用IK算法把部分从定性有问题的骨骼动画修正。比如穿高跟鞋=>不穿高跟鞋。大致步骤如下:
1. 为**Source/Target SkeletalMesh**分别设置**IK Rig**资产。
2. 在2个IK Rig资产设置用于重定向的**根骨骼**,一般是骨盆或者臀部骨骼。在对应骨骼上右键选择**Set Retarget Root**。
3. 为肢体部分分别创建骨骼链。
1. ![800](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230119105835.png)
4. 创建**IK Retargeter**资产。 需要设置**Source IKRig Asset**与**Target IKRig Asset**。
6. 在编辑器中的**Current Retarget Pose**设置重定向Pose如果为APose需要设置成TPose
7. 对于脚部部的穿帮可以设置**Fullbody IK**也可以是别的IK解算器与**IK Goal**并且在对应骨骼链中设置对应的IK Goal。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121171015.png)
最后导出想要重定向的动画。
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121171445.png)
PS.
1. 在**IK Retargeter Editor**的details面部中可以通过**Retarget IK**来预览IK Targeting的效果。
2. ThirdPerson模板采用FullBody IK进行IK Retarget。
### UE5实时重定向
在执行以上步骤之后在新SkeletalMesh的动画蓝图中使用**Retarget Pose From Mesh**节点。再设置**IK Retarget**资产即可。
需要注意角色类需要2个模型一起挂载其中一个模型隐藏并且将**Visibility Based Anim Tick Option** 选项设置成**Alway Tick Pose And Refresh Bone**
### Skeleton Compatibility
官方翻译为**可兼容的骨架**,不同骨骼之间共享动画的功能。
https://docs.unrealengine.com/5.0/en-US/skeletons-in-unreal-engine/#compatibleskeletons
![image.png](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20230121214547.png)
这个功能可以用来共享动画,并且不需要重定向。
## 其他
UE5的新功能实时重定向
https://www.bilibili.com/video/BV1HZ4y117Yk/?spm_id_from=333.999.0.0&vd_source=d47c0bb42f9c72fd7d74562185cee290
可以用来制作一些角色特有的动画。也可以用于共享动画。
- https://docs.unrealengine.com/5.0/zh-CN/facial-animation-sharing-in-unreal-engine/
- https://docs.unrealengine.com/5.0/zh-CN/using-sub-anim-instances-in-unreal-engine/