From f6fda856bff9834fc746fb86bb928ea8d1bdec77 Mon Sep 17 00:00:00 2001 From: cit110 <840418418@qq.com> Date: Sun, 26 Apr 2026 11:59:04 +0800 Subject: [PATCH] Add native retarget pose anim instance --- PHY.uproject | 4 + Source/PHY/PHY.Build.cs | 1 + .../PHYCharacterMeshBridgeComponent.cpp | 77 +++++++++-- .../Animation/PHYRetargetPoseAnimInstance.cpp | 130 ++++++++++++++++++ .../PHYCharacterMeshBridgeComponent.h | 11 +- .../Animation/PHYRetargetPoseAnimInstance.h | 88 ++++++++++++ 6 files changed, 300 insertions(+), 11 deletions(-) create mode 100644 Source/PHY/Private/Animation/PHYRetargetPoseAnimInstance.cpp create mode 100644 Source/PHY/Public/Animation/PHYRetargetPoseAnimInstance.h diff --git a/PHY.uproject b/PHY.uproject index 6e1609d..18c3676 100644 --- a/PHY.uproject +++ b/PHY.uproject @@ -26,6 +26,10 @@ "Name": "ModularGameplay", "Enabled": true }, + { + "Name": "IKRig", + "Enabled": true + }, { "Name": "GenericCombatSystem", "Enabled": true, diff --git a/Source/PHY/PHY.Build.cs b/Source/PHY/PHY.Build.cs index 38aded8..5ef1d6b 100644 --- a/Source/PHY/PHY.Build.cs +++ b/Source/PHY/PHY.Build.cs @@ -25,6 +25,7 @@ public class PHY : ModuleRules "GenericGameSystem", "GenericEffectsSystem", "AuroraDevs_UGC", + "IKRig", "AIModule" }); diff --git a/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp b/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp index ae6dcb1..e972905 100644 --- a/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp +++ b/Source/PHY/Private/Animation/PHYCharacterMeshBridgeComponent.cpp @@ -7,6 +7,7 @@ #include "Animation/AnimInstance.h" #include "Animation/PHYAnimationSettings.h" #include "Class/PHYClassComponent.h" +#include "Animation/PHYRetargetPoseAnimInstance.h" #include "Components/SkeletalMeshComponent.h" #include "Engine/SkeletalMesh.h" #include "GameFramework/Character.h" @@ -111,15 +112,7 @@ void UPHYCharacterMeshBridgeComponent::RefreshFollowPoseMode() DisplayMeshComponent->SetLeaderPoseComponent(nullptr); - const UPHYAnimationSettings* Settings = GetDefault(); - if (Settings && !Settings->DefaultDisplayAnimClass.IsNull() && !DisplayMeshComponent->GetAnimClass()) - { - if (UClass* DefaultDisplayAnimClass = Settings->DefaultDisplayAnimClass.LoadSynchronous()) - { - DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); - DisplayMeshComponent->SetAnimInstanceClass(DefaultDisplayAnimClass); - } - } + ConfigureRetargetFollowPose(); } USkeletalMeshComponent* UPHYCharacterMeshBridgeComponent::GetVisualMeshComponentForSocket(const FName SocketName) const @@ -180,3 +173,69 @@ bool UPHYCharacterMeshBridgeComponent::CanUseLeaderPose() const const USkeletalMesh* DisplayMesh = DisplayMeshComponent ? DisplayMeshComponent->GetSkeletalMeshAsset() : nullptr; return SourceMesh && DisplayMesh && SourceMesh->GetSkeleton() && SourceMesh->GetSkeleton() == DisplayMesh->GetSkeleton(); } + +void UPHYCharacterMeshBridgeComponent::ConfigureRetargetFollowPose() +{ + if (!SourceMeshComponent || !DisplayMeshComponent) + { + return; + } + + TSubclassOf RetargetAnimClass = ResolveRetargetDisplayAnimClass(); + if (!RetargetAnimClass) + { + UE_LOG(LogTemp, Warning, TEXT("PHYCharacterMeshBridgeComponent: DisplayMesh %s has no retarget AnimClass."), *GetNameSafe(DisplayMeshComponent)); + return; + } + + if (!RetargetAnimClass->IsChildOf(UPHYRetargetPoseAnimInstance::StaticClass())) + { + UE_LOG(LogTemp, Warning, TEXT("PHYCharacterMeshBridgeComponent: Display AnimClass %s is not based on UPHYRetargetPoseAnimInstance; falling back to native retarget instance."), *GetNameSafe(RetargetAnimClass.Get())); + RetargetAnimClass = UPHYRetargetPoseAnimInstance::StaticClass(); + } + + DisplayMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint); + if (DisplayMeshComponent->GetAnimClass() != RetargetAnimClass.Get()) + { + DisplayMeshComponent->SetAnimInstanceClass(RetargetAnimClass); + } + + if (!CurrentRetargeter.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("PHYCharacterMeshBridgeComponent: DisplayMesh %s needs an IK Retargeter for cross-skeleton follow pose."), *GetNameSafe(DisplayMeshComponent)); + return; + } + + UPHYRetargetPoseAnimInstance* RetargetAnimInstance = Cast(DisplayMeshComponent->GetAnimInstance()); + if (!RetargetAnimInstance) + { + UE_LOG(LogTemp, Warning, TEXT("PHYCharacterMeshBridgeComponent: DisplayMesh %s did not create a UPHYRetargetPoseAnimInstance."), *GetNameSafe(DisplayMeshComponent)); + return; + } + + RetargetAnimInstance->ConfigureRetargetPoseFromMesh(SourceMeshComponent, CurrentRetargeter); +} + +TSubclassOf UPHYCharacterMeshBridgeComponent::ResolveRetargetDisplayAnimClass() const +{ + if (!DisplayMeshComponent) + { + return nullptr; + } + + if (UClass* CurrentAnimClass = DisplayMeshComponent->GetAnimClass()) + { + return CurrentAnimClass; + } + + const UPHYAnimationSettings* Settings = GetDefault(); + if (Settings && !Settings->DefaultDisplayAnimClass.IsNull()) + { + if (UClass* DefaultDisplayAnimClass = Settings->DefaultDisplayAnimClass.LoadSynchronous()) + { + return DefaultDisplayAnimClass; + } + } + + return UPHYRetargetPoseAnimInstance::StaticClass(); +} diff --git a/Source/PHY/Private/Animation/PHYRetargetPoseAnimInstance.cpp b/Source/PHY/Private/Animation/PHYRetargetPoseAnimInstance.cpp new file mode 100644 index 0000000..6b02b17 --- /dev/null +++ b/Source/PHY/Private/Animation/PHYRetargetPoseAnimInstance.cpp @@ -0,0 +1,130 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Animation/PHYRetargetPoseAnimInstance.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYRetargetPoseAnimInstance) + +#include "Components/SkeletalMeshComponent.h" +#include "Retargeter/IKRetargeter.h" + +FPHYRetargetPoseAnimInstanceProxy::FPHYRetargetPoseAnimInstanceProxy(UAnimInstance* InAnimInstance, FAnimNode_RetargetPoseFromMesh* InRetargetNode) + : FAnimInstanceProxy(InAnimInstance) + , RetargetNode(InRetargetNode) +{ +} + +void FPHYRetargetPoseAnimInstanceProxy::Initialize(UAnimInstance* InAnimInstance) +{ + FAnimInstanceProxy::Initialize(InAnimInstance); + + if (RetargetNode) + { + FAnimationInitializeContext InitContext(this); + RetargetNode->Initialize_AnyThread(InitContext); + } +} + +void FPHYRetargetPoseAnimInstanceProxy::PreUpdate(UAnimInstance* InAnimInstance, float DeltaSeconds) +{ + FAnimInstanceProxy::PreUpdate(InAnimInstance, DeltaSeconds); + + if (RetargetNode && RetargetNode->HasPreUpdate()) + { + RetargetNode->PreUpdate(InAnimInstance); + } +} + +void FPHYRetargetPoseAnimInstanceProxy::CacheBones() +{ + if (bBoneCachesInvalidated && RetargetNode) + { + FAnimationCacheBonesContext Context(this); + RetargetNode->CacheBones_AnyThread(Context); + bBoneCachesInvalidated = false; + } +} + +bool FPHYRetargetPoseAnimInstanceProxy::Evaluate(FPoseContext& Output) +{ + if (!RetargetNode) + { + Output.ResetToRefPose(); + return true; + } + + RetargetNode->Evaluate_AnyThread(Output); + return true; +} + +void FPHYRetargetPoseAnimInstanceProxy::UpdateAnimationNode(const FAnimationUpdateContext& InContext) +{ + UpdateCounter.Increment(); + + if (RetargetNode) + { + RetargetNode->Update_AnyThread(InContext); + } +} + +void FPHYRetargetPoseAnimInstanceProxy::ConfigureRetargetPose(UIKRetargeter* InRetargeter, USkeletalMeshComponent* InSourceMeshComponent) +{ + if (!RetargetNode) + { + return; + } + + RetargetNode->IKRetargeterAsset = InRetargeter; + RetargetNode->RetargetFrom = ERetargetSourceMode::CustomSkeletalMeshComponent; + RetargetNode->SourceMeshComponent = InSourceMeshComponent; + RetargetNode->bSuppressWarnings = false; + + if (FIKRetargetProcessor* Processor = RetargetNode->GetRetargetProcessor()) + { + Processor->SetNeedsInitialized(); + } +} + +bool UPHYRetargetPoseAnimInstance::ConfigureRetargetPoseFromMesh(USkeletalMeshComponent* InSourceMeshComponent, FSoftObjectPath InRetargeterPath) +{ + RetargetSourceMeshComponent = InSourceMeshComponent; + RetargeterPath = InRetargeterPath; + LoadedRetargeter = nullptr; + + if (!RetargetSourceMeshComponent) + { + UE_LOG(LogTemp, Warning, TEXT("PHYRetargetPoseAnimInstance: SourceMeshComponent is missing.")); + return false; + } + + if (!RetargeterPath.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("PHYRetargetPoseAnimInstance: IK Retargeter path is missing for %s."), *GetNameSafe(GetOwningComponent())); + return false; + } + + UObject* LoadedObject = RetargeterPath.TryLoad(); + LoadedRetargeter = Cast(LoadedObject); + if (!LoadedRetargeter) + { + UE_LOG(LogTemp, Warning, TEXT("PHYRetargetPoseAnimInstance: %s is not a valid UIKRetargeter asset."), *RetargeterPath.ToString()); + return false; + } + + FPHYRetargetPoseAnimInstanceProxy& Proxy = GetProxyOnGameThread(); + Proxy.ConfigureRetargetPose(LoadedRetargeter, RetargetSourceMeshComponent); + return true; +} + +void UPHYRetargetPoseAnimInstance::NativeInitializeAnimation() +{ + Super::NativeInitializeAnimation(); + + FPHYRetargetPoseAnimInstanceProxy& Proxy = GetProxyOnGameThread(); + Proxy.Initialize(this); + Proxy.ConfigureRetargetPose(LoadedRetargeter, RetargetSourceMeshComponent); +} + +FAnimInstanceProxy* UPHYRetargetPoseAnimInstance::CreateAnimInstanceProxy() +{ + return new FPHYRetargetPoseAnimInstanceProxy(this, &RetargetNode); +} diff --git a/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h b/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h index e43716c..4edf583 100644 --- a/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h +++ b/Source/PHY/Public/Animation/PHYCharacterMeshBridgeComponent.h @@ -10,6 +10,7 @@ class ACharacter; class UAnimInstance; class UPHYClassComponent; +class UPHYRetargetPoseAnimInstance; class USkeletalMesh; class USkeletalMeshComponent; @@ -37,7 +38,7 @@ public: /** @brief 将新的显示 Mesh 应用到显示层,并按骨架关系刷新 Follow Pose 模式。 * @param NewDisplayMesh 新显示层 Skeletal Mesh。 * @param NewDisplayAnimClass 可选显示层 AnimClass,异骨架 Retarget 路径使用。 - * @param NewRetargeter 可选 Retargeter 软路径,首期只缓存不强制加载。 + * @param NewRetargeter 可选 Retargeter 软路径,异骨架 Retarget Pose 路径会加载并传递给显示层 AnimInstance。 * @return 应用成功返回 true。 */ UFUNCTION(BlueprintCallable, Category="PHY|Animation") @@ -77,6 +78,12 @@ protected: /** @brief 判断显示层是否可使用 Leader Pose 跟随源运动 Mesh。 */ bool CanUseLeaderPose() const; + /** @brief 为异骨架显示层配置 Retarget Pose From Mesh 动画实例。 */ + void ConfigureRetargetFollowPose(); + + /** @brief 获取异骨架显示层应使用的 AnimClass,缺省时回退到原生 Retarget AnimInstance。 */ + TSubclassOf ResolveRetargetDisplayAnimClass() const; + /** @brief 源运动 Mesh 组件,固定来自 ACharacter::GetMesh()。 */ UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") TObjectPtr SourceMeshComponent; @@ -85,7 +92,7 @@ protected: UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") TObjectPtr DisplayMeshComponent; - /** @brief 当前显示层 Retargeter 软路径,首期只作为后续 AnimBP/Retarget 配置入口。 */ + /** @brief 当前显示层 Retargeter 软路径,异骨架 Retarget Pose From Mesh 路径使用。 */ UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation") FSoftObjectPath CurrentRetargeter; }; diff --git a/Source/PHY/Public/Animation/PHYRetargetPoseAnimInstance.h b/Source/PHY/Public/Animation/PHYRetargetPoseAnimInstance.h new file mode 100644 index 0000000..973ec1f --- /dev/null +++ b/Source/PHY/Public/Animation/PHYRetargetPoseAnimInstance.h @@ -0,0 +1,88 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Animation/AnimInstance.h" +#include "Animation/AnimInstanceProxy.h" +#include "AnimNodes/AnimNode_RetargetPoseFromMesh.h" +#include "UObject/SoftObjectPath.h" +#include "PHYRetargetPoseAnimInstance.generated.h" + +class UIKRetargeter; +class USkeletalMeshComponent; + +/** + * @brief 显示层 Retarget Pose From Mesh 动画实例代理。 + * + * 代理直接驱动 UE IKRig 的 FAnimNode_RetargetPoseFromMesh,用于把 SourceMesh 的运动姿态重定向到显示层 Mesh。 + */ +USTRUCT() +struct PHY_API FPHYRetargetPoseAnimInstanceProxy : public FAnimInstanceProxy +{ + GENERATED_BODY() + + FPHYRetargetPoseAnimInstanceProxy() = default; + FPHYRetargetPoseAnimInstanceProxy(UAnimInstance* InAnimInstance, FAnimNode_RetargetPoseFromMesh* InRetargetNode); + + virtual void Initialize(UAnimInstance* InAnimInstance) override; + virtual void PreUpdate(UAnimInstance* InAnimInstance, float DeltaSeconds) override; + virtual void CacheBones() override; + virtual bool Evaluate(FPoseContext& Output) override; + virtual void UpdateAnimationNode(const FAnimationUpdateContext& InContext) override; + + /** @brief 配置 Retarget 节点使用的源 Mesh 与 IK Retargeter。 */ + void ConfigureRetargetPose(UIKRetargeter* InRetargeter, USkeletalMeshComponent* InSourceMeshComponent); + +private: + FAnimNode_RetargetPoseFromMesh* RetargetNode = nullptr; +}; + +/** + * @brief PHY 显示层原生 Retarget Pose 动画实例。 + * + * MeshBridge 在异骨架显示层路径中使用该类,把 ACharacter::GetMesh() 的运动姿态通过 IK Retargeter 输出到 DisplayMesh。 + */ +UCLASS(Transient, BlueprintType) +class PHY_API UPHYRetargetPoseAnimInstance : public UAnimInstance +{ + GENERATED_BODY() + +public: + /** + * @brief 设置 Retarget Pose From Mesh 的运行时输入。 + * @param InSourceMeshComponent 提供运动姿态的源 Mesh 组件。 + * @param InRetargeterPath IK Retargeter 资产软路径。 + * @return SourceMesh 与 Retargeter 均有效时返回 true。 + */ + UFUNCTION(BlueprintCallable, Category="PHY|Animation") + bool ConfigureRetargetPoseFromMesh(USkeletalMeshComponent* InSourceMeshComponent, FSoftObjectPath InRetargeterPath); + + /** @brief 获取当前源 Mesh 组件。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + USkeletalMeshComponent* GetRetargetSourceMeshComponent() const { return RetargetSourceMeshComponent; } + + /** @brief 获取当前 IK Retargeter 资产路径。 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Animation") + FSoftObjectPath GetRetargeterPath() const { return RetargeterPath; } + +protected: + virtual void NativeInitializeAnimation() override; + virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override; + +private: + /** @brief Retarget Pose From Mesh 运行时节点。 */ + UPROPERTY() + FAnimNode_RetargetPoseFromMesh RetargetNode; + + /** @brief 当前用于重定向的源 Mesh 组件。 */ + UPROPERTY(Transient) + TObjectPtr RetargetSourceMeshComponent; + + /** @brief 当前使用的 IK Retargeter 资产。 */ + UPROPERTY(Transient) + TObjectPtr LoadedRetargeter; + + /** @brief 当前 IK Retargeter 资产软路径。 */ + UPROPERTY(Transient) + FSoftObjectPath RetargeterPath; +};