《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://www.pswp.cn/web/96397.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96397.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HttpServletRequest vs ServletContext 全面解析

HttpServletRequest vs ServletContext 全面解析 一、 核心区别概览特性HttpServletRequest (请求对象)ServletContext (Servlet上下文/应用对象)作用域请求范围应用范围生命周期从客户端发出请求开始,到服务器返回响应结束。从Web应用启动(部署&#xf…

Java后端工程师如何学AI

Java后端工程师如何学AI 目录 前言为什么Java后端工程师要学习AIAI学习路径规划基础知识体系实践项目建议学习资源推荐学习时间规划常见问题与解决方案职业发展建议总结 前言 随着人工智能技术的快速发展,AI已经不再是计算机科学专业的专属领域。作为Java后端工…

Django REST Framework 中 @action 装饰器详解

概述 action 装饰器是 Django REST Framework (DRF) 中 ViewSet 的一个核心功能,用于定义自定义路由方法。它允许开发者在标准的 CRUD 操作(list、create、retrieve、update、destroy)之外,创建符合特定业务需求的接口&#xff0c…

【重磅更新】RetroBoard 全面升级,让敏捷回顾更高效、更安全、更贴心!

​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​…

中州养老:华为云设备管理接口开发全流程

需求分析点击同步数据时,要把华为云的数据拉取到我们的系统中对于新增设备操作,实际上这些参数与华为云产品我们添加设备时的参数是一样的表结构设计E-R图数据库字段接口分析对于设备中的数据,我们既要再IOT平台存储,又要在数据库中存储.之所以保存两份数据的原因:IOT平台中只是…

Llama-Factory微调Qwen2.5-VL从数据集制作到部署记录

Llama-Factory微调Qwen2.5-VL从数据集制作到部署记录 电脑环境配置: 1.ubuntu24 2.3090(24G) 3.Cuda12.9 一、数据集制作 我的数据集主要是对图像内容进行描述 1.Label-studio制作数据集 这是最原始的从零开始制作数据集的方法,不建议这样做!…

【蓝桥杯真题67】C++数位和为偶数的数 第十五届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解

C++数位和为偶数的数 第十五届蓝桥杯青少年创意编程大赛C++选拔赛真题 博主推荐 所有考级比赛学习相关资料合集【推荐收藏】 1、C++专栏 电子学会C++一级历年真题解析 电子学会C++二级历年真题解析

【计算机网络 | 第11篇】宽带接入技术及其发展历程

文章目录宽带接入技术详解数字传输系统技术演进早期电话网的传输技术演变数字传输系统技术演进:从碎片到统一宽带接入技术 ADSLADSL的基本原理与非对称特性DMT调制技术:多子信道并行传输ADSL接入网组成电话分离器的设计原理与优势ADSL的升级:…

(论文速读)SCSegamba:用于结构裂纹分割的轻量级结构感知视觉曼巴

论文题目:SCSegamba: Lightweight Structure-Aware Vision Mamba for Crack Segmentation in Structures(用于结构裂纹分割的轻量级结构感知视觉曼巴)会议:CVPR2025摘要:不同场景下的结构裂缝像素级分割仍然是一个相当…

《苏超风云》亮相时尚大赏,成短剧行业发展新风向

当男频短剧凭借《一品布衣》五天横扫10亿播放的数据宣告逆袭,短剧市场格局正经历深刻洗牌。风口之下,头条视听、中皋文旅、国内时尚视觉与短视频创作领域的头部厂牌“大湾视频”携手下场,打造精品男频短剧《苏超风云》,剑指2025年…

HTML5新年元旦网站源码

新年主题网站开发概述 本项目基于HTML5、CSS3与JavaScript技术栈,打造了一个功能丰富、交互体验流畅的新年主题网站,涵盖文化展示、互动娱乐与社交分享三大核心模块,通过现代化前端技术实现沉浸式节日氛围营造。 1.1、核心功能架构 网站采…

CentOS 7 下iscsi存储服务配置验证

一、环境说明 centos7服务器*2服务器ip:服务端10.10.10.186 客户端10.10.10.184服务端存储卷sda1提前关闭防火墙,或开放默认 iSCSI 使用 3260 端口 二、服务端(Target)配置 安装 iSCSI target 服务 yum install -y targetcli syst…

立即数、栈、汇编与C函数的调用

一、立即数在 ARM 架构中,立即数是指在指令中直接编码的常量值,而不是通过寄存器或内存引用的值立即数的特点编码限制:ARM指令是固定长度的(32位),因此立即数不能占用太多位数。典型的算术和逻辑指令通常只…

贪心算法与动态规划:数学原理、实现与优化

贪心算法与动态规划:数学原理、实现与优化 引言:算法选择的本质 在计算机科学领域,算法选择的本质是对问题特征的数学建模与求解策略的匹配。贪心算法与动态规划作为两种经典的优化算法,分别在不同问题域展现出独特优势。本文将从…

Leetcode 刷题记录 21 —— 技巧

Leetcode 刷题记录 21 —— 技巧 本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C语言,08及以后为Java语言&#xf…

Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示

把这两个开关打开,然后刷新gradle文件

Java中方法重写与重载的区别

目录 1. 方法重载 (Overload) 什么是方法重载? 重载的特点: 重载的示例: 重载的调用: 2. 方法重写 (Override) 什么是方法重写? 重写的特点: 重写的示例: 重写的调用: 3.…

微信小程序发送订阅消息-一次订阅,一直发送消息。

实现思路长期订阅要求太高,需要政府、公共交通等单位才有资格,所以只能使用一次性订阅。 就像是买奶茶,下单以后,会弹出让用户订阅消息那种。以买奶茶为例:用户第一次下单成功,点击了订阅消息。(一般都有三…

408 Request Timeout:请求超时,服务器等待客户端发送请求的时间过长。

408 Request Timeout 是 HTTP 状态码之一,表示客户端在发送请求时,服务器等待的时间过长,最终放弃了处理该请求。此问题通常与网络延迟、客户端配置、服务器设置或者应用程序的性能有关。1. 常见原因1.1 客户端问题网络连接延迟或不稳定&…

MongoDB面试集锦

该书的使用的MongoDB版本是 4.2.01、什么是NoSQL数据库?NoSQL和RDBMS有什么区别?在那些情况下使用和不使用NoSQL数据库?NoSQL是非关系型数据库,NoSQLNot Only SQL 。关系型数据库采用的是结构化的数据,NoSQL采用的是键…