当前位置: 首页 > news >正文

《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 创建左手插槽,获取相对变换

  1. 在之前的测试中,我们可以观察到人物角色的左手并未放置在正确的持枪位置,IK 将允许我们调整人物角色手臂上、下的骨骼,以便我们的左手能够正确放置。
    在这里插入图片描述

  2. 对于不同的武器,人物角色持枪手放置的位置都不同,因此我们可以为武器添加一个插槽,这个插槽将作为持枪手放置的位置,我们希望无论是什么武器,只要调整插槽位置,持枪手都能放置在正确的位置上。打开骨骼网格体 “Assault_Rifle_A” 的编辑器,在左侧骨骼树面板中,为骨骼节点 “Root_Bone1” 添加插槽,重命名为 “LeftHandSocket”,这个插槽便是左手正确的持枪位置。
    在这里插入图片描述

  3. 打开 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)*/
    
  4. 在 “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)*/...
    }
    
  5. 我们需要获取装备的武器的骨骼网格体以访问插槽,但由于武器类 “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)*/
    };
    
  6. 在 “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)*/
    }
    

    TransformFromBoneSpaceTransformToBoneSpace 是两个UE4中的C++函数,用于在骨骼空间(Bone Space)和世界空间(World Space)之间转换位置和旋转。它们的用法是:
    TransformFromBoneSpace(BoneName, InPosition, InRotation, OutPosition, OutRotation)
    TransformToBoneSpace(BoneName, InPosition, InRotation, OutPosition, OutRotation)
    其中,BoneName 是一个 FName 类型的参数,表示要转换的骨骼的名称。InPositionInRotation 是两个 FVector 类型的参数,表示要转换的位置和旋转。OutPositionOutRotation 是两个 FVector 类型的引用参数,表示转换后的位置和旋转。
    TransformFromBoneSpace 函数的作用是,将一个位置和旋转从骨骼空间(相对于父骨骼的局部坐标系)转换到世界空间(绝对坐标系)。这个函数在需要将骨骼的局部变换同步到世界空间时较为常用,比如在 PoseableMeshComponent 中设置骨骼的位置和旋转。
    TransformToBoneSpace 函数的作用是,将一个位置和旋转从世界空间转换到骨骼空间。这个函数在需要将世界空间的变换应用到骨骼空间时较为常用,比如在 SkeletalMeshComponent 中获取骨骼的位置和旋转。


    —— B站《4_The Weapon武器》

53.2 在蓝图中应用 FABRIK

  1. 编译后,打开虚幻引擎,在人物动画蓝图类 “BlasterAnimBP” 的 “AnimGraph” 事件面板中新建状态机节点 “FABRIK”,然后添加蓝图节点 “新保存的缓存姿势”,重命名为 “FABRIK”,并与状态机节点 “FABRIK” 进行连接。
    在这里插入图片描述

  2. 双击状态机节点 “FABRIK” 进入编辑界面,在面板中从 “Entry” 引出一条线,连接新的状态节点 “FABRIK”。
    在这里插入图片描述

  3. 双击状态节点 “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”,顶端骨骼是最开始进行解算的骨骼,根骨骼是解算结束的骨骼。
    在这里插入图片描述
    alt text

  4. 在 “AnimGraph” 事件面板中用 “使用缓存姿势"FABRIK"” 替代 “使用缓存姿势“Equipped””,与 “每个骨骼的分层混合” 节点进行连接,编译、保存。


53.3 调整左手插槽的位置

  1. 进行测试,在人物角色持枪后调整摄像机视角,在骨骼网格体 “Assault_Rifle_A” 的编辑器中对 “LeftHandSocket” 进行平移,使得左手放置在正确的持枪位置上。这里可以像教学视频中那样,在人物角色蓝图类 “BP_EpicCharacter” 中调整骨骼网格体的角度,方便我们进行对照。
    在这里插入图片描述
    在这里插入图片描述

  2. 将关卡 “BlasterMap” 中的武器蓝图类实例 “BP_Weapon” 骨骼网格体换成手枪 “Pistols_A”,然后在骨骼网格体 “Pistols_A” 的编辑器中,为骨骼节点 “Root_Bone1” 添加插槽 “LeftHandSocket” 并对其进行平移,使得左手放置在正确的持枪位置上。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  3. 将 “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” 插槽位置,确保了左手能够正确放置在各种武器的持枪位置上。最终,人物角色的左手能够放置在不同武器的正确持枪位置,提供了更加真实和准确的持枪动画表现。
在这里插入图片描述



文章转载自:

http://bw452ZU8.jkfyt.cn
http://QZuSIPeN.jkfyt.cn
http://vGANo9cj.jkfyt.cn
http://Hq7LacSM.jkfyt.cn
http://b9Xi4jiQ.jkfyt.cn
http://kIMWvapS.jkfyt.cn
http://i4insjai.jkfyt.cn
http://HEjxxiEA.jkfyt.cn
http://h7Fl56oA.jkfyt.cn
http://GJzcs02A.jkfyt.cn
http://7js2iF1L.jkfyt.cn
http://ll9evQ6r.jkfyt.cn
http://trHnqllB.jkfyt.cn
http://tZCNUavr.jkfyt.cn
http://XqYl2LNA.jkfyt.cn
http://Z6oM77DT.jkfyt.cn
http://ElvEVE7i.jkfyt.cn
http://FdCXzXcZ.jkfyt.cn
http://QZDmD18V.jkfyt.cn
http://7MiUGT7q.jkfyt.cn
http://zXFyk0vO.jkfyt.cn
http://9Ye792u7.jkfyt.cn
http://rXFWlRqG.jkfyt.cn
http://pfQAAan7.jkfyt.cn
http://v6B9bTTs.jkfyt.cn
http://C0B4b6bR.jkfyt.cn
http://fCn7LUKW.jkfyt.cn
http://EGLhB4Vy.jkfyt.cn
http://qIJ9sFDr.jkfyt.cn
http://LdYyQKVl.jkfyt.cn
http://www.dtcms.com/a/378523.html

相关文章:

  • 网络编程套接字(UDP)
  • Git子模块(Submodule)合并冲突的原理与解决方案
  • 谷粒商城项目-P16快速开发-人人开源搭建后台管理系统
  • 记一次nginx服务器安全防护实战之“恶意目录探测攻击”防护
  • 突破多模态极限!InstructBLIP携指令微调革新视觉语言模型,X-InstructBLIP实现跨模态推理新高度
  • 如何在实际应用中平衡YOLOv12的算力需求和检测精度?
  • MySQL 主键约束:表的 “身份证”,数据完整性的核心保障
  • 分布式事务性能优化:从故障现场到方案落地的实战手记(二)
  • 本地生活服务平台创新模式观察:积分体系如何重塑消费生态?
  • 内存传输速率MT/s
  • ThinkPHP8学习篇(六):数据库(二)
  • Synchronized原理解析
  • Cesium深入浅出之shadertoy篇
  • LoRaWAN网关支持双NS的场景有哪些?
  • BigVGAN:探索 NVIDIA 最新通用神经声码器的前沿
  • SpringTask和XXL-job概述
  • 软考系统架构设计师之软件维护篇
  • 从CTF题目深入变量覆盖漏洞:extract()与parse_str()的陷阱与防御
  • 第五章:Python 数据结构:列表、元组与字典(二)
  • Flow Matching Guide and Code(3)
  • 内存泄漏一些事
  • 嵌入式学习day47-硬件-imx6ul-LED、Beep
  • 【数据结构】队列详解
  • C++/QT
  • GPT 系列论文1-2 两阶段半监督 + zero-shot prompt
  • 昆山精密机械公司8个Solidworks共用一台服务器
  • MasterGo钢笔Pen
  • 【算法--链表】143.重排链表--通俗讲解
  • 数据库的回表
  • 《Learning Langchain》阅读笔记13-Agent(1):Agent Architecture