컨텐츠 검색
[UE5] FABRIK로 무기 소켓 기반 Left Hand IK 구현하기

2026. 6. 9. 21:40구현

1. 구현 목표

  • 무기 소켓 위치에 캐릭터의 왼손을 자연스럽게 고정
  • 무기 종류가 바뀌어도 동일한 Hand IK 구조 재사용
  • 애니메이션 재생 중 손이 무기에서 벗어나는 문제 개선

2. 구현 이유

  • 기본 사격 애니메이션만으로는 무기별 손 위치가 맞지 않는 문제
  • 무기 메시/소켓 기준으로 손 위치를 보정해야 함

3. 구현 방식 후보 비교

몬스터가 총을 사용하는 애니메이션을 구현하기 위해 처음부터 모든 애니메이션을 직접 제작하는 방식보다는, 기존에 검증된 샘플 애니메이션을 활용하는 방향을 선택했다.

현재 프로젝트에서는 Lyra Sample Animation을 리타겟팅해서 몬스터 캐릭터에 적용했다.

하지만 리타겟팅 과정에서 원본 캐릭터와 현재 몬스터 캐릭터의 스켈레톤 구조, 팔 길이, 어깨 위치, 손목 방향이 완전히 같지 않았다.

그 결과 오른손은 무기를 잡고 있지만, 왼손은 총기의 손잡이 위치와 미묘하게 어긋나는 문제가 발생했다.

특히 사격 자세에서 왼손이 공중에 떠 있거나, 무기와 맞지 않는 위치에 놓이면서 총을 들고 쏘는 애니메이션이 어색해 보였다.

이 문제를 해결하기 위해 몇 가지 방식을 비교했다.

3.1 무기별 전용 애니메이션 제작

첫 번째 방법은 무기별로 전용 애니메이션을 제작하는 것이다.
라이플, 샷건, 권총처럼 무기마다 그립 위치와 사격 자세가 다르기 때문에, 각 무기에 맞는 전용 Idle, Aim, Fire 애니메이션을 제작하면 가장 자연스러운 결과를 얻을 수 있다.

장점

무기별로 가장 자연스러운 자세를 만들 수 있다.
애니메이션 자체에서 양손의 위치, 팔꿈치 방향, 손목 회전, 상체 실루엣까지 직접 조정할 수 있기 때문에 결과물의 품질이 높다.

또한 IK 보정에 의존하지 않아도 되므로 특정 포즈에서 팔이 꺾이거나 손목이 어긋나는 문제를 줄일 수 있다.
캐릭터와 무기에 맞춰 제작된 애니메이션이라면 별도의 보정 없이도 좋은 결과를 기대할 수 있다.

단점

문제는 제작 비용이다.
현재 나는 애니메이터가 아니기 때문에 무기별 전용 애니메이션을 직접 자연스럽게 제작하는 데 많은 시간이 필요하다.

특히 현재 프로젝트에서 중요한 개발 목표는 적 AI의 전투 패턴, 추적, 사격, 상태 전환 등을 구현하는 것이다.

애니메이션 제작에 시간을 많이 사용하면 적 AI 시스템을 구현하고 다듬는 시간이 줄어든다.

또한 무기 수가 늘어나면 애니메이션 리소스도 함께 증가한다.
무기마다 Idle, Aim, Fire, Reload, Move 같은 애니메이션을 따로 관리해야 하므로 유지보수 부담도 커진다.

제외한 이유

무기별 전용 애니메이션 자체를 완전히 제외한 것은 아니다.
장기적으로는 무기마다 전용 애니메이션을 사용하는 것이 가장 자연스러운 방식이라고 생각한다.

하지만 현재 단계에서는 직접 애니메이션을 제작하는 것보다, Lyra Sample Animation을 리타겟팅해서 기본 사격 자세를 빠르게 확보하는 것이 더 효율적이었다.

그리고 리타겟팅 과정에서 발생한 왼손 위치 오차는 FABRIK를 이용해 보정하는 방식이 개발 시간 대비 가장 현실적인 선택이었다.

즉, 이번 구현에서는 전용 애니메이션 제작 대신 리타겟팅 애니메이션 + FABRIK 보정 구조를 선택했다.


3.2 Two Bone IK 사용

두 번째 방법은 Two Bone IK를 사용하는 것이다.
Two Bone IK는 상완, 하완, 손처럼 2개의 본 체인을 가진 팔이나 다리에 자주 사용되는 IK 방식이다.

왼팔의 경우 일반적으로 upperarm_l, lowerarm_l, hand_l 구조를 가지기 때문에 Two Bone IK를 적용하기 좋은 구조다.
무기 소켓 위치를 Effector로 사용하면 왼손을 총기의 손잡이 위치에 맞출 수 있다.

장점

팔처럼 2개의 관절로 구성된 구조에 적합하다.
설정이 비교적 단순하고, 팔꿈치 방향을 Joint Target으로 제어할 수 있어 팔이 꺾이는 방향을 예측하기 쉽다.

또한 단순히 “손을 특정 위치에 둔다”는 목적이라면 Two Bone IK만으로도 충분히 구현할 수 있다.
왼손을 무기 소켓 위치에 붙이는 기본적인 Hand IK에는 적합한 방식이다.

단점

Two Bone IK는 팔꿈치 방향을 직접 제어해야 하는 장점이 있지만, 반대로 Joint Target 설정이 어긋나면 팔꿈치가 의도하지 않은 방향으로 꺾일 수 있다.

현재 문제는 단순히 손의 위치만 맞추는 것이 아니라, Lyra 애니메이션을 몬스터 스켈레톤에 리타겟팅하면서 생긴 양손 간 위치 차이를 자연스럽게 줄이는 것이었다.

이 과정에서는 기본 애니메이션 포즈를 최대한 유지하면서 왼팔 체인이 무기 소켓을 따라가도록 보정하는 방식이 필요했다.

Two Bone IK도 충분히 가능한 선택지였지만, 팔꿈치 방향, 손목 회전, 소켓 기준 보정까지 함께 조정해야 했기 때문에 초기 설정 부담이 있었다.

FABRIK 대신 사용하지 않은 이유

Two Bone IK는 왼팔 Hand IK에 적합한 방식이다.
하지만 이번 구현에서는 리타겟팅된 애니메이션의 어긋난 왼손 위치를 빠르게 보정하고, 무기 소켓을 기준으로 왼팔 체인이 따라오게 만드는 것이 우선이었다.

FABRIK는 Root Bone부터 Tip Bone까지 지정한 체인을 목표 위치에 맞추는 방식이기 때문에, 특정 무기 소켓 위치를 기준으로 손을 보정하는 구조를 만들기 쉬웠다.

또한 upperarm_l 또는 lowerarm_l을 Root로 두고 hand_l을 Tip으로 설정해 적용 범위를 조절할 수 있었다.

따라서 Two Bone IK도 가능했지만, 이번 작업에서는 FABRIK가 리타겟팅 애니메이션의 손 위치 오차를 빠르게 보정하는 데 더 적합하다고 판단했다.


3.3 FABRIK 사용

최종적으로 선택한 방식은 FABRIK를 이용해 왼손을 무기 소켓 위치에 맞추는 것이다.

무기 메시에는 왼손이 잡아야 할 위치에 S_LeftHandIK 소켓을 추가했다.

그리고 AnimInstance에서 현재 장착 중인 무기의 소켓 Transform을 가져온 뒤, Animation Blueprint의 FABRIK 노드에서 Effector Transform으로 사용했다.

이렇게 하면 리타겟팅된 Lyra 애니메이션을 기본 자세로 사용하면서도, 왼손만 무기 소켓 위치에 맞게 보정할 수 있다.

장점

가장 큰 장점은 애니메이션 제작 시간을 줄이면서도 시각적인 어색함을 개선할 수 있다는 점이다.
Lyra Sample Animation을 리타겟팅해 기본 사격 자세를 확보하고, 리타겟팅으로 인해 어긋난 왼손 위치만 FABRIK로 보정할 수 있었다.

무기별로 왼손 위치가 다르더라도, 각 무기에 S_LeftHandIK 소켓만 올바르게 배치하면 동일한 IK 구조를 재사용할 수 있다.
즉, 무기마다 애니메이션을 새로 만들지 않아도 소켓 위치 조정만으로 왼손 그립 위치를 맞출 수 있다.

또한 FABRIK는 Root Bone과 Tip Bone 사이의 체인을 기준으로 목표 위치를 맞추기 때문에 IK 적용 범위를 조절하기 쉽다.
예를 들어 upperarm_l부터 hand_l까지 적용하면 왼팔 전체가 보정되고, lowerarm_l부터 hand_l까지 적용하면 팔꿈치 아래쪽을 중심으로 보정할 수 있다.

현재 프로젝트처럼 애니메이션보다 AI 구현에 더 많은 시간을 투자해야 하는 상황에서는, FABRIK를 이용한 보정 방식이 개발 시간 대비 효율적이었다.

단점

FABRIK를 사용한다고 해서 모든 문제가 자동으로 해결되는 것은 아니다.
기본 애니메이션 포즈와 무기 소켓 위치의 차이가 너무 크면 팔이 몸을 관통하거나, 손목이 부자연스럽게 꺾일 수 있다.

또한 소켓의 위치뿐 아니라 회전도 중요하다.
왼손이 소켓 위치에는 붙었지만 손목이 틀어져 보이는 경우, FABRIK 설정뿐만 아니라 무기 소켓의 회전값이나 Effector Transform Space를 함께 확인해야 한다.

리타겟팅된 애니메이션 자체의 기본 포즈가 많이 어긋나 있다면 FABRIK는 보정 수단일 뿐, 완전한 해결책은 아니다.
이 경우에는 리타겟팅 설정, IK Retargeter의 Pose, 무기 장착 위치, 소켓 위치를 함께 조정해야 한다.

최종 선택 이유

이번 구현에서 가장 중요한 목표는 적 AI 개발 시간을 확보하면서도, 몬스터의 사격 애니메이션이 어색해 보이지 않도록 만드는 것이었다.

직접 무기별 전용 애니메이션을 제작하면 가장 자연스러운 결과를 얻을 수 있지만, 현재 단계에서는 애니메이션 제작에 많은 시간을 쓰기 어렵다.

그래서 Lyra Sample Animation을 리타겟팅해 기본 사격 애니메이션을 확보하고, 리타겟팅 과정에서 발생한 왼손 위치 오차는 FABRIK로 보정하는 방식을 선택했다.

최종적으로 FABRIK를 선택한 이유는 다음과 같다.

  • 직접 애니메이션 제작 시간을 줄일 수 있음
  • Lyra 리타겟팅 애니메이션을 재사용할 수 있음
  • 스켈레톤 차이로 발생한 왼손 위치 오차를 보정할 수 있음
  • 무기 소켓 기준으로 왼손 위치를 맞출 수 있음
  • 무기별로 소켓만 조정하면 동일한 AnimGraph 구조를 재사용할 수 있음
  • 적 AI 개발에 더 많은 시간을 투자할 수 있음
  • 서버 복제 없이 클라이언트 애니메이션 보정으로 처리 가능

따라서 이번 프로젝트에서는 Lyra Sample Animation 리타겟팅 + FABRIK 기반 Left Hand IK 보정 구조가 가장 현실적인 선택이었다.

이 방식은 전용 애니메이션만큼 완벽한 결과를 보장하지는 않지만, 제한된 개발 시간 안에서 사격 자세의 어색함을 줄이고, AI 개발에 집중할 수 있는 균형 잡힌 방법이라고 판단했다.

4. 전체 구조

이번 Hand IK는 무기 소켓을 기준으로 왼손 위치를 보정하는 구조로 구현했다.

무기 Mesh에는 왼손이 잡아야 할 위치를 나타내는 `S_LeftHandIK` 소켓을 추가했다.
Enemy Character는 현재 장착 중인 무기 Actor를 참조하고, AnimInstance는 이 무기에서 `S_LeftHandIK` 소켓 Transform을 가져온다.

가져온 소켓 Transform은 그대로 FABRIK에 전달하지 않고, 캐릭터 Mesh 기준 좌표로 변환해서 사용한다.
월드 좌표 그대로 사용하면 캐릭터의 위치나 회전이 바뀔 때 왼손 위치가 어긋날 수 있기 때문이다.

Animation Blueprint에서는 기본 Locomotion Pose를 먼저 계산한 뒤, 마지막 단계에서 FABRIK를 적용한다.
이렇게 하면 리타겟팅된 Lyra 애니메이션의 기본 자세는 유지하면서, 왼손만 무기 소켓 위치에 맞게 보정할 수 있다.

전체 흐름은 다음과 같다.

Weapon Mesh
└─ S_LeftHandIK Socket
↓
Enemy Character
└─ CurrentWeapon 참조
↓
Enemy AnimInstance
└─ LeftHandIKTransform 계산
↓
Animation Blueprint
└─ FABRIK Node
↓
Final Pose

5. 에디터 설정

5.1 무기 Skeletal Mesh 소켓 추가

소켓 추가 후, Locaiton과 Rotation도 수정해주면서 자연스러운 손 위치를 잡아줘야 한다.

5.2 캐릭터 Skeleton 확인

이번 구현에서 필요한 주요 Bone은 upperarm_l, lowerarm_l, hand_l 이다.

5.3 Animation Blueprint Preview

애니메이션을 프리뷰로 실행했을 때 실제 왼쪽 손과 무기와의 간격이다.

6. C++ 구현

6.1 Weapon 클래스에 Left Hand IK Socket 제공

// NSEnemyWeaponBase.h
public:
    bool TryGetLeftHandIKTransform(FTransform& OutTransform) const;

protected:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|IK")
    FName LeftHandIKSocketName = TEXT("S_LeftHandIK");


// NSEnemyWeaponBase.cpp
bool ANSEnemyWeaponBase::TryGetLeftHandIKTransform(FTransform& OutTransform) const
{
    if (!IsValid(WeaponMesh) ||
        LeftHandIKSocketName.IsNone() ||
        !WeaponMesh->DoesSocketExist(LeftHandIKSocketName))
    {
        return false;
    }

    OutTransform = WeaponMesh->GetSocketTransform(
        LeftHandIKSocketName,
        RTS_World);

    return true;
}

6.2 WeaponComponent 에서 현재 장착 무기 관리

// NSEnemyWeaponComponent.h
public:
    virtual void GetLifetimeReplicatedProps(
        TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    ANSEnemyWeaponBase* GetCurrentWeapon() const { return CurrentWeapon; }

private:
    UPROPERTY(Transient, Replicated)
    TObjectPtr<ANSEnemyWeaponBase> CurrentWeapon;

// NSEnemyWeaponComponent.cpp
UNSEnemyWeaponComponent::UNSEnemyWeaponComponent()
{
    PrimaryComponentTick.bCanEverTick = false;
    SetIsReplicatedByDefault(true);
}

void UNSEnemyWeaponComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(UNSEnemyWeaponComponent, CurrentWeapon);
}


// NSEnemyCharacterBase.cpp
ANSEnemyWeaponBase* ANSEnemyCharacterBase::GetCurrentWeapon() const
{
    return WeaponComponent ? WeaponComponent->GetCurrentWeapon() : nullptr;
}

6.3 AnimInstance에서 Left Hand IK Transform 갱신

// NSEnemyAnimInstance.h
protected:
    void UpdateLeftHandIK(float DeltaSeconds);
    void UpdateLeftHandIKAlpha(float TargetAlpha, float DeltaSeconds);

    UPROPERTY(BlueprintReadOnly, Category = "Animation|Hand IK")
    float LeftHandIKAlpha = 0.0f;

    UPROPERTY(BlueprintReadOnly, Category = "Animation|Hand IK")
    FTransform LeftHandIKTransform = FTransform::Identity;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation|Hand IK", meta = (ClampMin = "0.0"))
    float LeftHandIKInterpSpeed = 12.0f;

// NSEnemyAnimInstance.cpp
void UNSEnemyAnimInstance::UpdateLeftHandIK(float DeltaSeconds)
{
    float TargetAlpha = 0.0f;

    if (!IsValid(EnemyCharacter) || EnemyCharacter->IsDead())
    {
        UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
        return;
    }

    const UAbilitySystemComponent* ASC = EnemyCharacter->GetAbilitySystemComponent();
    if (!IsValid(ASC) || !ASC->HasMatchingGameplayTag(NSGameplayTags::State_Enemy_Combat))
    {
        UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
        return;
    }

    const ANSEnemyWeaponBase* CurrentWeapon = EnemyCharacter->GetCurrentWeapon();

    if (!IsValid(CurrentWeapon))
    {
        UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
        return;
    }

    FTransform SocketWorldTransform;
    if (!CurrentWeapon->TryGetLeftHandIKTransform(SocketWorldTransform))
    {
        UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
        return;
    }

    USkeletalMeshComponent* MeshComponent = GetOwningComponent();

    if (!IsValid(MeshComponent) ||
        !MeshComponent->DoesSocketExist(TEXT("hand_r")))
    {
        UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
        return;
    }

    FVector BoneSpaceLocation = FVector::ZeroVector;
    FRotator BoneSpaceRotation = FRotator::ZeroRotator;

    MeshComponent->TransformToBoneSpace(
        TEXT("hand_r"),
        SocketWorldTransform.GetLocation(),
        SocketWorldTransform.Rotator(),
        BoneSpaceLocation,
        BoneSpaceRotation);

    LeftHandIKTransform = FTransform(
        BoneSpaceRotation,
        BoneSpaceLocation,
        FVector::OneVector);

    TargetAlpha = 1.0f;
    UpdateLeftHandIKAlpha(TargetAlpha, DeltaSeconds);
}

void UNSEnemyAnimInstance::UpdateLeftHandIKAlpha(float TargetAlpha, float DeltaSeconds)
{
    LeftHandIKAlpha = FMath::FInterpTo(
        LeftHandIKAlpha,
        TargetAlpha,
        DeltaSeconds,
        LeftHandIKInterpSpeed);

    if (LeftHandIKAlpha <= UE_KINDA_SMALL_NUMBER)
    {
        LeftHandIKAlpha = 0.0f;
    }
}

7. Animation Blueprint 구성

FABRIK 노드 설정

9. 트러블 슈팅

FABRIK를 적용하면서 왼손 위치 보정 자체는 가능했지만, 실제 애니메이션에 적용하는 과정에서 몇 가지 문제가 발생했다.
특히 리타겟팅된 Lyra 애니메이션을 몬스터 스켈레톤에 적용한 상태였기 때문에, 단순히 손을 소켓 위치에 붙이는 것만으로는 자연스러운 결과가 나오지 않았다.

9.1 왼손이 무기 소켓에 붙지만 손목이 꺾이는 문제

문제

FABRIK를 적용한 뒤 왼손이 무기의 S_LeftHandIK 소켓 위치에는 정상적으로 붙었다.
하지만 손목의 회전이 자연스럽지 않아, 손이 무기를 잡는 것이 아니라 꺾인 상태로 붙어 있는 것처럼 보였다.

즉, 위치 보정은 되었지만 회전 방향이 맞지 않는 문제가 있었다.

원인

FABRIK의 Effector Transform은 위치뿐 아니라 회전 정보도 함께 사용할 수 있다.
이때 무기 소켓의 회전값이 왼손 본의 방향과 맞지 않으면, 손은 소켓 위치로 이동하지만 손목이 부자연스럽게 틀어질 수 있다.

현재 문제는 FABRIK 설정 자체보다는 S_LeftHandIK 소켓의 위치와 회전이 왼손이 잡아야 할 기준 방향과 맞지 않았기 때문에 발생했다.

해결

무기 Mesh에 추가한 S_LeftHandIK 소켓의 위치와 회전을 직접 조정했다.
왼손이 무기 손잡이를 자연스럽게 감싸는 위치로 소켓을 이동시키고, 손목이 꺾이지 않도록 소켓 회전값을 맞췄다.

특히 단순히 소켓 위치만 맞추는 것이 아니라, 왼손의 손바닥 방향과 손목 방향이 무기 그립 방향과 자연스럽게 이어지도록 회전값을 함께 조정했다.

정리

이 문제는 코드나 FABRIK 노드 설정만으로 해결하기보다는, 무기 소켓의 위치와 회전을 애니메이션 결과를 보면서 반복적으로 조정해야 했다.

최종적으로 S_LeftHandIK 소켓을 왼손이 실제로 잡아야 할 기준점으로 사용하도록 수정하면서 손목이 꺾이는 문제를 완화할 수 있었다.

9.2 팔이 몸 안을 관통하는 문제

문제

FABRIK를 적용했을 때 왼손이 무기 소켓 위치로 이동하면서, 왼팔이 몸 안을 관통하는 문제가 발생했다.

특히 팔을 자연스럽게 올리는 전용 애니메이션이 없는 상태에서 FABRIK Alpha 값을 보간해 손 위치를 맞추다 보니, 팔이 기존 포즈에서 목표 위치로 이동하는 중간 과정이 부자연스럽게 보였다.

원인

현재 구조에서는 팔을 아래에서 위로 자연스럽게 올리는 별도의 전환 애니메이션을 사용하지 않았다.
대신 FABRIK의 Alpha 값을 보간해서 왼손이 점진적으로 무기 소켓 위치에 붙도록 처리했다.

이 방식은 구현이 간단하고 빠르게 적용할 수 있지만, 기본 애니메이션 포즈와 목표 IK 위치의 차이가 큰 경우 중간 자세를 세밀하게 제어하기 어렵다.
그 결과 왼팔이 무기 위치로 이동하는 과정에서 몸을 관통하는 문제가 발생했다.

해결 여부

이 문제는 현재 완전히 해결하지 못했다.

근본적으로 해결하려면 팔을 자연스럽게 들어 올리는 전환 애니메이션이 필요하다.
예를 들어 순찰 상태에서 전투 상태로 전환될 때, 총을 들어 조준 자세로 들어가는 애니메이션을 먼저 재생하고, 그 이후 FABRIK를 적용하는 방식이 더 자연스럽다.

하지만 현재 프로젝트에서는 애니메이션 제작보다 적 AI 개발에 우선순위를 두고 있었기 때문에, 별도의 팔 올리기 애니메이션을 제작하지 않았다.
따라서 현재는 FABRIK Alpha 보간으로 최대한 어색함을 줄이는 수준에서 처리했다.

개선 방향

추후 개선한다면 다음과 같은 방식으로 해결할 수 있다.

  • 정찰 상태에서 전투 상태로 전환되는 전용 애니메이션 추가
  • 총을 들어 올리는 Montage 또는 State Machine 전환 애니메이션 제작
  • FABRIK Alpha를 전환 애니메이션 후반부터 적용
  • 기본 포즈와 IK 목표 위치의 차이를 줄이기 위해 리타겟팅 포즈 재조정
  • 필요하다면 Control Rig를 사용해 팔꿈치 방향과 손목 회전을 더 세밀하게 제어

현재 단계에서는 미해결 문제로 남아 있지만, 원인과 개선 방향은 확인한 상태다.

9.3 에디터 Preview에서는 맞는데 PIE에서 어긋나는 문제

문제

Animation Blueprint의 Preview에서는 왼손이 무기 소켓에 맞게 보이지만, 실제 PIE 실행 시에는 왼손 위치가 어긋나는 문제가 발생했다.

Preview에서는 소켓 위치와 FABRIK 결과가 정상적으로 보였지만, 게임 실행 환경에서는 캐릭터의 실제 무기 장착 상태나 Transform 계산 결과가 Preview와 다르게 나타났다.

현재 상태

이 문제는 아직 완전히 해결하지 못했다.

현재 의심되는 원인은 Preview 환경과 PIE 환경에서 참조하는 무기 Mesh, 장착 Socket, Transform 갱신 타이밍, 좌표계 변환 기준이 다르기 때문이다.

특히 AnimInstance에서 무기 소켓 Transform을 가져오는 시점과 실제 무기가 캐릭터 손에 Attach되는 시점이 어긋나면, PIE에서 IK 위치가 예상과 다르게 계산될 수 있다.

확인이 필요한 부분

추후에는 다음 항목들을 확인할 필요가 있다.

  • Preview에서 사용하는 무기 Mesh와 PIE에서 실제 장착되는 무기 Mesh가 같은지 확인
  • 무기가 hand_r 또는 지정한 장착 소켓에 정상적으로 Attach되는지 확인
  • S_LeftHandIK 소켓 위치가 실제 런타임 무기 Mesh에도 동일하게 적용되어 있는지 확인
  • AnimInstance에서 소켓 Transform을 가져오는 시점이 무기 Attach 이후인지 확인
  • FABRIK Effector Transform Space가 코드에서 변환한 좌표계와 일치하는지 확인
  • TransformToBoneSpace 기준 Bone과 FABRIK Effector Target이 같은지 확인
  • PIE에서 캐릭터 Mesh의 상대 회전이나 스케일이 Preview와 다른지 확인

개선 방향

현재는 Preview와 PIE 결과가 다르게 나오는 원인을 추적 중이다.
가장 먼저 확인할 부분은 S_LeftHandIK 소켓의 World Transform을 Debug Draw로 시각화하고, PIE에서 실제 왼손이 따라가야 하는 위치가 어디인지 확인하는 것이다.

그 다음 AnimInstance에서 계산한 LeftHandIKTransform 값과 FABRIK 노드의 Effector Space 설정이 일치하는지 확인할 필요가 있다.

이 문제는 아직 해결 전이지만, 단순히 FABRIK 노드 문제라기보다는 Preview 환경과 실제 런타임 환경의 Transform 기준 차이에서 발생한 문제로 보고 있다.

10. 성능 및 네트워크 고려

FABRIK 기반 Hand IK는 게임 로직보다는 시각적인 애니메이션 보정에 가까운 기능이다.
따라서 서버에서 왼손 위치를 계산하거나 복제하지 않고, 각 클라이언트의 Animation Blueprint에서 로컬로 처리하는 구조로 구현했다.

이번 구현에서 네트워크와 성능 측면에서 고려한 부분은 다음과 같다.


10.1 IK 계산은 AnimInstance에서 처리

왼손 IK 위치는 AnimInstance에서 현재 장착된 무기의 S_LeftHandIK 소켓 Transform을 가져와 계산한다.
이 값은 Animation Blueprint의 FABRIK 노드에서 Effector Transform으로 사용된다.

즉, Hand IK 계산 흐름은 다음과 같다.

현재 장착 무기
→ S_LeftHandIK 소켓 Transform 조회
→ hand_r 기준 Bone Space로 변환
→ AnimInstance 변수에 저장
→ AnimGraph의 FABRIK 노드에서 사용

이 과정은 애니메이션 표현을 위한 계산이므로, 서버 권한이 필요한 로직으로 보지 않았다.
몬스터의 실제 전투 상태, 사격 여부, 데미지 판정은 서버에서 처리하고, 왼손 IK는 클라이언트에서 보여지는 자세 보정으로 분리했다.


10.2 서버에서 IK Transform을 복제하지 않는 이유

왼손 IK Transform은 매 프레임 변할 수 있는 애니메이션 보정 값이다.
이 값을 서버에서 계산하고 클라이언트로 복제하면 불필요한 네트워크 비용이 발생한다.

특히 Hand IK는 게임 판정에 직접 영향을 주는 값이 아니다.
왼손이 무기 소켓에 정확히 붙어 있는지는 시각적인 품질 문제이지, 데미지 판정이나 AI 의사결정에 필요한 정보가 아니다.

따라서 다음 값들은 복제하지 않았다.

- LeftHandIKTransform
- FABRIK Alpha
- 왼손 본 위치
- 왼손 본 회전

대신 네트워크에서 중요한 값만 복제하는 방식이 적합하다.

- 현재 장착 무기
- 사격 상태
- 전투 상태
- 몬스터 위치 / 회전
- Montage 또는 Anim State에 필요한 상태 값

클라이언트는 복제된 무기 장착 상태를 바탕으로 자신의 AnimInstance에서 소켓 위치를 계산하고, FABRIK를 적용한다.


10.3 무기 장착 상태만 Replication

Hand IK를 위해 필요한 핵심 정보는 “현재 어떤 무기를 장착하고 있는가”이다.
무기 Actor 또는 장착된 무기 Mesh가 클라이언트에도 동일하게 존재한다면, 클라이언트는 해당 무기의 S_LeftHandIK 소켓 위치를 직접 계산할 수 있다.

따라서 서버에서는 무기 장착 결과만 복제하고, IK 보정 자체는 클라이언트에서 계산하도록 했다.

Server
- 몬스터 무기 장착 처리
- CurrentWeapon 복제
- 사격 / 전투 상태 관리

Client
- 복제된 CurrentWeapon 확인
- 무기 소켓 Transform 계산
- FABRIK로 왼손 위치 보정

이 구조를 사용하면 네트워크로 매 프레임 손 위치를 보내지 않아도 된다.
또한 무기별로 S_LeftHandIK 소켓만 올바르게 설정되어 있다면, 클라이언트에서도 서버와 동일한 시각적 결과를 얻을 수 있다.


10.4 FABRIK 계산 비용

FABRIK는 AnimGraph에서 매 프레임 계산된다.
따라서 모든 캐릭터에게 무조건 복잡한 IK를 적용하면 비용이 증가할 수 있다.

하지만 이번 구현에서는 왼팔 체인 하나에만 FABRIK를 적용했다.
대상 본도 upperarm_l 또는 lowerarm_l부터 hand_l까지로 제한되기 때문에, 계산 범위가 크지 않다.

그래도 몬스터 수가 많아질 경우를 고려해 다음과 같은 점을 주의해야 한다.

- 불필요한 상황에서는 FABRIK Alpha를 0으로 낮추기
- 무기를 들고 있지 않은 상태에서는 IK 계산하지 않기
- 멀리 있는 몬스터는 Update Rate Optimization 고려
- 화면에 보이지 않는 몬스터는 애니메이션 Tick 비용 줄이기
- Max Iterations 값을 과도하게 높이지 않기

FABRIK의 Max Iterations는 목표 위치에 도달하기 위한 반복 횟수다.
값을 높이면 정확도는 올라갈 수 있지만 계산 비용도 증가한다.

이번 경우에는 왼손을 무기 소켓에 맞추는 정도이기 때문에, 과도한 반복 횟수를 사용할 필요는 없다고 판단했다.


10.5 Tick 비용 최소화

Hand IK Transform은 매 프레임 갱신될 수 있지만, 불필요한 연산은 줄이는 것이 좋다.

예를 들어 현재 장착 무기가 없거나, 몬스터가 전투 상태가 아니거나, 사격 자세를 취하지 않는 경우에는 IK 계산을 생략할 수 있다.

if (!bUseLeftHandIK || !EquippedWeapon)
{
    LeftHandIKAlpha = 0.0f;
    return;
}

이런 식으로 조건을 먼저 확인하면 무기 소켓 Transform 조회나 Bone Space 변환을 불필요하게 수행하지 않아도 된다.

다만 Animation Blueprint의 Event Graph나 NativeUpdateAnimation은 프레임마다 호출될 수 있으므로, 그 안에서 무거운 탐색 로직을 반복하지 않는 것이 중요하다.

피해야 할 방식은 다음과 같다.

- 매 프레임 GetAllActorsOfClass로 무기 탐색
- 매 프레임 Cast를 여러 번 반복
- 매 프레임 소켓 이름을 문자열로 새로 생성
- 유효하지 않은 무기 참조를 계속 접근

장착 무기 참조는 캐릭터가 관리하고, AnimInstance는 캐릭터에서 이미 보유 중인 참조만 가져와 사용하는 식으로 구성하는 것이 좋다.


10.6 LOD와 원거리 몬스터 처리

가까운 거리에서는 왼손이 무기를 제대로 잡고 있는지가 잘 보인다.
하지만 멀리 있는 몬스터는 손 위치의 작은 어긋남이 거의 보이지 않는다.

따라서 몬스터 수가 많아지는 상황에서는 거리 기반으로 IK 적용 강도를 줄이거나, 특정 LOD 이후에는 Hand IK를 비활성화하는 것도 고려할 수 있다.

예를 들어 다음과 같은 방식이 가능하다.

- 가까운 거리: FABRIK Alpha 1.0
- 중간 거리: FABRIK Alpha 감소
- 먼 거리: FABRIK 비활성화

또는 Skeletal Mesh의 Update Rate Optimization을 사용해 멀리 있는 캐릭터의 애니메이션 갱신 빈도를 줄일 수 있다.

이번 프로젝트에서는 몬스터 수와 화면 노출 범위를 고려했을 때, 우선 기본 FABRIK 적용만으로도 큰 성능 문제는 없다고 판단했다.
다만 추후 다수의 몬스터가 동시에 등장하는 상황에서는 LOD 기반 IK 비활성화를 적용할 수 있다.


10.7 게임 판정과 애니메이션 보정 분리

Hand IK는 어디까지나 시각적인 보정이다.
총알 발사 위치, 데미지 판정, AI 사격 조건은 왼손 위치에 의존하지 않도록 분리했다.

예를 들어 실제 투사체 발사 위치는 왼손이 아니라 무기 Muzzle Socket을 기준으로 처리한다.

시각 보정
- 왼손이 무기를 자연스럽게 잡도록 FABRIK 적용

게임 판정
- 총알 발사 위치는 Muzzle Socket 기준
- 데미지 판정은 서버 권한으로 처리
- AI 사격 조건은 Behavior Tree / Gameplay Ability 기준으로 처리

이렇게 분리하면 왼손 IK가 약간 어긋나더라도 게임 판정에는 영향을 주지 않는다.
네트워크 환경에서도 서버는 실제 전투 로직만 책임지고, 클라이언트는 애니메이션 품질을 보정하는 역할을 담당하게 된다.


10.8 정리

이번 Hand IK 구현에서는 성능과 네트워크 비용을 줄이기 위해 다음과 같은 방향을 선택했다.

- FABRIK는 클라이언트 AnimGraph에서 처리
- LeftHandIKTransform은 복제하지 않음
- 서버는 무기 장착 상태와 전투 상태만 관리
- 클라이언트는 복제된 무기 정보를 바탕으로 소켓 Transform 계산
- 게임 판정은 Muzzle Socket과 서버 로직 기준으로 처리
- Hand IK는 시각 보정 역할로 한정

결과적으로 FABRIK는 네트워크 동기화 대상이 아니라, 각 클라이언트에서 계산되는 로컬 애니메이션 보정으로 두는 것이 적합하다고 판단했다.

이 구조는 서버의 네트워크 부담을 줄이면서도, 플레이어가 보는 몬스터의 사격 자세를 자연스럽게 만드는 데 충분했다.

11. 결과