《UE5_C++多人TPS完整教程》学习笔记52 ——《P53 FABRIK 算法(FABRIK IK)》
本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P53 FABRIK 算法(FABRIK IK) 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P53 FABRIK 算法(FABRIK IK)
- 53.1 创建左手插槽,获取相对变换
- 53.2 在蓝图中应用 FABRIK
- 53.3 调整左手插槽的位置
- 53.4 Summary
P53 FABRIK 算法(FABRIK IK)
本节课我们将采用 FABRIK 算法解决之前人物角色左手未正确放置在持枪位置的问题。IK(Inverse kinematics,运动动力学逆运算)是机器人或动画中控制骨骼的位置的方法 ,而 FABRIK 算法是一种实现动力学逆运算的快速简单的迭代算法,英文全称为 Forward And Backward Reaching Inverse Kinematics,即前进和后退实现动力学逆运算。
53.1 创建左手插槽,获取相对变换
-
在之前的测试中,我们可以观察到人物角色的左手并未放置在正确的持枪位置,IK 将允许我们调整人物角色手臂上、下的骨骼,以便我们的左手能够正确放置。
-
对于不同的武器,人物角色持枪手放置的位置都不同,因此我们可以为武器添加一个插槽,这个插槽将作为持枪手放置的位置,我们希望无论是什么武器,只要调整插槽位置,持枪手都能放置在正确的位置上。打开骨骼网格体 “
Assault_Rifle_A
” 的编辑器,在左侧骨骼树面板中,为骨骼节点 “Root_Bone1
” 添加插槽,重命名为 “LeftHandSocket
”,这个插槽便是左手正确的持枪位置。
-
打开 Visual Studio,在 “
ABlasterCharaccter
” 类中声明并定义函数 “GetEquippedWeapon()
”,用于获取人物角色装备的武器,/*** BlasterCharaccter.h ***/...UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter {GENERATED_BODY()...public: ...FORCEINLINE float GetAO_Yaw() const { return AO_Yaw; } // 内联函数,用以访问 AO_YawFORCEINLINE float GetAO_Pitch() const { return AO_Pitch; } // 内联函数,用以访问 AO_Pitch /* P53 FABRIK 算法(FABRIK IK)*/AWeapon* GetEquippedWeapon(); // 获取人物角色装备的武器/* P53 FABRIK 算法(FABRIK IK)*/ };
/*** BlasterCharaccter.cpp ***/.../* P53 FABRIK 算法(FABRIK IK)*/ // 获取人物角色装备的武器 AWeapon* ABlasterCharacter::GetEquippedWeapon() {if (Combat == nullptr) return nullptr;return Combat->EquippedWeapon; // 访问枪战组件,获取装备的武器 } /* P53 FABRIK 算法(FABRIK IK)*/
-
在 “
BlasterAnimInstance.h
” 中声明 “AWeapon
” 武器类变量 “EquippedWeapon
” 和左持枪手变换(位置、旋转)变量 “LeftHandTransform
”,然后在 “BlasterAnimInstance.cpp
” 中调用上文定义的函数 “GetEquippedWeapon()
” 获取人物角色装备的武器。/*** BlasterAnimInstance.h ***/...UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance {GENERATED_BODY()...private:...UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bWeaponEquipped; // 是否装备了武器/* P53 FABRIK 算法(FABRIK IK)*/class AWeapon* EquippedWeapon; // 装备的武器/* P53 FABRIK 算法(FABRIK IK)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bIsCrouched; // 是否在蹲伏...UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float AO_Yaw; // 瞄准偏移偏航角UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;float AO_Pitch; // 瞄准偏移俯仰角/* P53 FABRIK 算法(FABRIK IK)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;FTransform LeftHandTransform; // 左持枪手变换/* P53 FABRIK 算法(FABRIK IK)*/ };
/*** BlasterAnimInstance.cpp ***/...void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {Super::NativeUpdateAnimation(DeltaTime); // 调用父类 AnimInstance 的 NativeUpdateAnimation() 函数...bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 BlasterCharacter 的 IsWeaponEquipped() 函数判断人物角色是否装备了武器/* P53 FABRIK 算法(FABRIK IK)*/EquippedWeapon = BlasterCharacter->GetEquippedWeapon(); // 调用 BlasterCharacter 的 GetEquippedWeapon() 函数获取人物角色装备的武器/* P53 FABRIK 算法(FABRIK IK)*/... }
-
我们需要获取装备的武器的骨骼网格体以访问插槽,但由于武器类 “
AWeapon
” 的骨骼网格体是私有变量,因此需要在 “Weapon.h
” 中添加内联函数 “GetWeaponMesh()
” 获取装备的武器的骨骼网格体。/*** Weapon.h ***/...UCLASS() class BLASTER_API AWeapon : public AActor {GENERATED_BODY()...public:void SetWeaponState(EWeaponState State); // 设置武器状态// forceinline 是编程中用于强制内联函数的关键字或注解,主要用于减少函数调用开销,但需谨慎使用以避免代码膨胀或性能下降。FORCEINLINE USphereComponent* GetAreaSphere() const { return AreaSphere; } // 获取武器球体/* P53 FABRIK 算法(FABRIK IK)*/FORCEINLINE USkeletalMeshComponent* GetWeaponMesh() const { return WeaponMesh; }/* P53 FABRIK 算法(FABRIK IK)*/ };
-
在 “
BlasterAnimInstance.cpp
” 中添加头文件 “Weapon.h
”,我们在前面的课程《UE5_C++多人TPS完整教程》学习笔记38 ——《P39 装备武器(Equipping Weapons)》中将武器附加至 “hand_r
” 的骨骼插槽 “RightHandSocket
” 上,武器在运行时不应该相对右手进行调整或移动,因此右手是骨骼空间的一个参考系(A reference frame in bone space),我们在获取左手插槽 “LeftHandSocket
” 在世界空间的变换 “LeftHandTransform
” 后,通过调用函数 “BlasterCharacter->GetMesh()->TransformToBoneSpace()
”,将 “LeftHandTransform
” 的位置和旋转转换成右手 “hand_r
” 骨骼空间参考系下的位置和旋转,用于后续动画蓝图中的 IK 计算,这样即使右手移动,左手也可以根据这个相对变换调整位置,以保持持枪姿势。
/*** BlasterAnimInstance.cpp ***/.../* P53 FABRIK 算法(FABRIK IK)*/ #include "Blaster/Weapon/Weapon.h" /* P53 FABRIK 算法(FABRIK IK)*/...void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) {Super::NativeUpdateAnimation(DeltaTime); // 调用父类 AnimInstance 的 NativeUpdateAnimation() 函数...bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 BlasterCharacter 的 IsWeaponEquipped() 函数判断人物角色是否装备了武器/* P53 FABRIK 算法(FABRIK IK)*/EquippedWeapon = BlasterCharacter->GetEquippedWeapon(); // 调用 BlasterCharacter 的 GetEquippedWeapon() 函数获取人物角色装备的武器/* P53 FABRIK 算法(FABRIK IK)*/.../* P53 FABRIK 算法(FABRIK IK)*/if (bWeaponEquipped && EquippedWeapon && EquippedWeapon->GetWeaponMesh() && BlasterCharacter->GetMesh()) {// 获取左手插槽 LeftHandSocket 的位置和旋转,存储在 LeftHandTransform 中LeftHandTransform = EquippedWeapon->GetWeaponMesh()->GetSocketTransform(FName("LeftHandSocket"), // 按插槽名称获取插槽变换ERelativeTransformSpace::RTS_World); // 相对变换空间为世界场景// FABRIK 实现的核心,在一个稳定、不受角色整体移动和旋转影响的参考系(hand_r 的骨骼空间)下,计算 LeftHandSocket 相对于这个参考系的位置和旋转FVector OutPosition; // LeftHandSocket 相对于 hand_r 的位置FRotator OutRotation; // LeftHandSocket 相对于 hand_r 的旋转BlasterCharacter->GetMesh()->TransformToBoneSpace(FName("hand_r"), // 目标骨骼空间,用作参考系 LeftHandTransform.GetLocation(), // 输入:世界空间中 LeftHandSocket 的位置,即左手正确的持枪位置FRotator::ZeroRotator, // 输入:零旋转体,因为实际上后面并没有使用这个输入旋转,输出旋转是由变换计算得到的OutPosition, // 输出:LeftHandSocket 相对于 hand_r 的位置OutRotation); // 输出:LeftHandSocket 相对于 hand_r 的旋转// 为什么是转换到 hand_r 的骨骼空间而不是 hand_l 的?// 因为武器是由右手持握的,所以武器的左手插槽 LeftHandSocket 的位置实际上是相对于右手骨骼的。// 通过将这个变换转换到右手的骨骼空间,我们可以得到一个固定的相对变换,// 这样即使右手移动,左手也可以根据这个相对变换调整位置,以保持持枪姿势。// 将转换后的位置和旋转设置回 LeftHandTransform,这个变换将用于后续动画蓝图中的 IK 计算LeftHandTransform.SetLocation(OutPosition); LeftHandTransform.SetRotation(FQuat(OutRotation)); }/* P53 FABRIK 算法(FABRIK IK)*/ }
TransformFromBoneSpace
和TransformToBoneSpace
是两个UE4中的C++函数,用于在骨骼空间(Bone Space)和世界空间(World Space)之间转换位置和旋转。它们的用法是:
TransformFromBoneSpace(BoneName, InPosition, InRotation, OutPosition, OutRotation)
TransformToBoneSpace(BoneName, InPosition, InRotation, OutPosition, OutRotation)
其中,BoneName
是一个FName
类型的参数,表示要转换的骨骼的名称。InPosition
和InRotation
是两个FVector
类型的参数,表示要转换的位置和旋转。OutPosition
和OutRotation
是两个FVector
类型的引用参数,表示转换后的位置和旋转。
TransformFromBoneSpace
函数的作用是,将一个位置和旋转从骨骼空间(相对于父骨骼的局部坐标系)转换到世界空间(绝对坐标系)。这个函数在需要将骨骼的局部变换同步到世界空间时较为常用,比如在PoseableMeshComponent
中设置骨骼的位置和旋转。
TransformToBoneSpace
函数的作用是,将一个位置和旋转从世界空间转换到骨骼空间。这个函数在需要将世界空间的变换应用到骨骼空间时较为常用,比如在SkeletalMeshComponent
中获取骨骼的位置和旋转。
—— B站《4_The Weapon武器》
53.2 在蓝图中应用 FABRIK
-
编译后,打开虚幻引擎,在人物动画蓝图类 “
BlasterAnimBP
” 的 “AnimGraph
” 事件面板中新建状态机节点 “FABRIK
”,然后添加蓝图节点 “新保存的缓存姿势”,重命名为 “FABRIK
”,并与状态机节点 “FABRIK
” 进行连接。
-
双击状态机节点 “
FABRIK
” 进入编辑界面,在面板中从 “Entry
” 引出一条线,连接新的状态节点 “FABRIK
”。
-
双击状态节点 “
FABRIK
” 进入编辑界面,在面板中添加蓝图节点 “使用缓存姿势"Aim Offsets"”、“本地到组件空间”(Local To Component)、“FABRIK”、“获取 Left Hand Transform”(Get Left Hand Transform)和“组件空间到本地”(Component To Local),绘制下图所示的蓝图。然后选择 “FABRIK” 节点,在右侧细节面板将 “结束效果器”(END EFFECTOR)选项卡下的 “执行器器目标”(Effector Target)设为 “hand_r
”,设置 “执行器变换空间”(Effector transform Space)为 “骨骼空间”(Bone Space),设置 “执行器旋转源”(Effector Rotatioin Source)为 “无变化(保留现有组件空间旋转)”(No Change (Preserve Existing Component Space Rotation));将 “解算器”(SOLVER) 选项卡下设置 “顶端骨骼”(Tip Bone)为 “hand_l
”,设置 “根骨骼”(Root Bone)为 “upperarm_l
”,顶端骨骼是最开始进行解算的骨骼,根骨骼是解算结束的骨骼。
-
在 “
AnimGraph
” 事件面板中用 “使用缓存姿势"FABRIK"” 替代 “使用缓存姿势“Equipped””,与 “每个骨骼的分层混合” 节点进行连接,编译、保存。
53.3 调整左手插槽的位置
-
进行测试,在人物角色持枪后调整摄像机视角,在骨骼网格体 “
Assault_Rifle_A
” 的编辑器中对 “LeftHandSocket
” 进行平移,使得左手放置在正确的持枪位置上。这里可以像教学视频中那样,在人物角色蓝图类 “BP_EpicCharacter
” 中调整骨骼网格体的角度,方便我们进行对照。
-
将关卡 “
BlasterMap
” 中的武器蓝图类实例 “BP_Weapon
” 骨骼网格体换成手枪 “Pistols_A
”,然后在骨骼网格体 “Pistols_A
” 的编辑器中,为骨骼节点 “Root_Bone1
” 添加插槽 “LeftHandSocket
” 并对其进行平移,使得左手放置在正确的持枪位置上。
-
将 “
BP_Weapon
” 骨骼网格体换回突击步枪 “Assault_Rifle_A
”。
53.4 Summary
本节课我们利用了虚幻引擎中的 FABRIK(Forward And Backward Reaching Inverse Kinematics)方法进行骨骼节点逆运动学运算,解决人物角色左手持枪位置不正确的问题。
首先,我们在武器骨骼网格体上创建了 “LeftHandSocket
” 插槽,作为左手正确的持枪位置参考点,然后通过C++代码获取该插槽的世界空间变换,并使用 “TransformToBoneSpace
” 函数将其转换到右手骨骼空间,得到一个稳定的相对变换参考系。接着,我们在动画蓝图中添加了虚幻引擎自带的 FABRIK 蓝图节点,并为其配置了正确的骨骼链参数,通过传入之前的相对变换, FABRIK 蓝图节点可以对左手进行逆运动学运算。随后,通过调整不同武器(突击步枪 “Assault_Rifle_A
” 和手枪 “Pistols_A
”)的 “LeftHandSocket
” 插槽位置,确保了左手能够正确放置在各种武器的持枪位置上。最终,人物角色的左手能够放置在不同武器的正确持枪位置,提供了更加真实和准确的持枪动画表现。