Add native retarget pose anim instance

This commit is contained in:
2026-04-26 11:59:04 +08:00
parent f8169a63e9
commit f6fda856bf
6 changed files with 300 additions and 11 deletions

View File

@@ -26,6 +26,10 @@
"Name": "ModularGameplay",
"Enabled": true
},
{
"Name": "IKRig",
"Enabled": true
},
{
"Name": "GenericCombatSystem",
"Enabled": true,

View File

@@ -25,6 +25,7 @@ public class PHY : ModuleRules
"GenericGameSystem",
"GenericEffectsSystem",
"AuroraDevs_UGC",
"IKRig",
"AIModule"
});

View File

@@ -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<UPHYAnimationSettings>();
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<UAnimInstance> 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<UPHYRetargetPoseAnimInstance>(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<UAnimInstance> UPHYCharacterMeshBridgeComponent::ResolveRetargetDisplayAnimClass() const
{
if (!DisplayMeshComponent)
{
return nullptr;
}
if (UClass* CurrentAnimClass = DisplayMeshComponent->GetAnimClass())
{
return CurrentAnimClass;
}
const UPHYAnimationSettings* Settings = GetDefault<UPHYAnimationSettings>();
if (Settings && !Settings->DefaultDisplayAnimClass.IsNull())
{
if (UClass* DefaultDisplayAnimClass = Settings->DefaultDisplayAnimClass.LoadSynchronous())
{
return DefaultDisplayAnimClass;
}
}
return UPHYRetargetPoseAnimInstance::StaticClass();
}

View File

@@ -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<UIKRetargeter>(LoadedObject);
if (!LoadedRetargeter)
{
UE_LOG(LogTemp, Warning, TEXT("PHYRetargetPoseAnimInstance: %s is not a valid UIKRetargeter asset."), *RetargeterPath.ToString());
return false;
}
FPHYRetargetPoseAnimInstanceProxy& Proxy = GetProxyOnGameThread<FPHYRetargetPoseAnimInstanceProxy>();
Proxy.ConfigureRetargetPose(LoadedRetargeter, RetargetSourceMeshComponent);
return true;
}
void UPHYRetargetPoseAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
FPHYRetargetPoseAnimInstanceProxy& Proxy = GetProxyOnGameThread<FPHYRetargetPoseAnimInstanceProxy>();
Proxy.Initialize(this);
Proxy.ConfigureRetargetPose(LoadedRetargeter, RetargetSourceMeshComponent);
}
FAnimInstanceProxy* UPHYRetargetPoseAnimInstance::CreateAnimInstanceProxy()
{
return new FPHYRetargetPoseAnimInstanceProxy(this, &RetargetNode);
}

View File

@@ -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<UAnimInstance> ResolveRetargetDisplayAnimClass() const;
/** @brief 源运动 Mesh 组件,固定来自 ACharacter::GetMesh()。 */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation")
TObjectPtr<USkeletalMeshComponent> SourceMeshComponent;
@@ -85,7 +92,7 @@ protected:
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation")
TObjectPtr<USkeletalMeshComponent> DisplayMeshComponent;
/** @brief 当前显示层 Retargeter 软路径,首期只作为后续 AnimBP/Retarget 配置入口。 */
/** @brief 当前显示层 Retargeter 软路径,异骨架 Retarget Pose From Mesh 路径使用。 */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category="PHY|Animation")
FSoftObjectPath CurrentRetargeter;
};

View File

@@ -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<USkeletalMeshComponent> RetargetSourceMeshComponent;
/** @brief 当前使用的 IK Retargeter 资产。 */
UPROPERTY(Transient)
TObjectPtr<UIKRetargeter> LoadedRetargeter;
/** @brief 当前 IK Retargeter 资产软路径。 */
UPROPERTY(Transient)
FSoftObjectPath RetargeterPath;
};