2015년 10월 12일

커스텀 애니메이션 노드 만들기

저자: Lina Halper

애님 블루프린트는 알파 값에 따라 다양한 애니메이션들을 블랜딩 할 수 있게 해 줍니다. 문서에서 애님 블루프린트 노드에 관한 더 자세한 사항을 읽어보실 수 있습니다.

여기서는 여러분이 필요로 하는 기본 노드들을 제공하지만, 여러분은 실제로 작업할 때 커스텀 노드가 필요하실 것입니다. 만드는 것은 굉장히 쉽지만, 기본 시스템이 어떻게 디자인 되었는지를 이해하는 것이 필요합니다. 문서 링크에서 시스템 에 관한 심도깊은 지식을 제공하지만, 많은 분들이 여기서 고생을 하는 것을 보아왔기 때문에 기본에 충실하게 설명하고자 합니다.

위 링크에서 설명되어 있듯, 시스템은 두 개의 클래스를 필요로 합니다. 하나는 에디터에서 보일 그래프 노드와 실제 런타임에서 사용될 비헤이비어 노드입니다. 저희는 최적화 과정을 위해 이 것들을 나누게 되었습니다. 노드 컨스트럭션은 굉장히 비용이 비싼 편이고, 400 노드 이상을 가진 20개의 캐릭터가 30초마다 죽고 다시 스폰되는 것은 메모리와 CPU에 굉장한 부담이 됩니다. 애님 블루프린트를 이용해서 스폰을 한다면, 캐릭터는 그래프 노드 없이 비헤이비어 노드만 가지고 스폰 될 것입니다.

애님 그래프 노드와 애님 비헤이비어 노드를 비교해 보도록 하겠습니다.

애님 그래프 노드

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base

Anim Behavior Node 

struct ENGINE_API FAnimNode_SequencePlayer : public FAnimNode_Base

둘이 서로 베이스 클래스가 다르다는 것을 눈치채셨을 것입니다. 하나는 UObject이고 나머지 하나는 UStruct입니다. 이 블로그에서 저희는 첫 번째를 애님 그래프 노드라고 부르고, 두 번째 것을 애님 비헤이비어 노드라고 부르겠습니다. 모든 그래프 노드는 아래와 같이 연 관된 비헤이비어 노드를 갖습니다.

class UAnimGraphNode_SequencePlayer : public UAnimGraphNode_Base
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(EditAnywhere, Category=Settings)
    FAnimNode_SequencePlayer Node;

}

애님 그래프 노드는 비헤이비어 노드에 대해서 알고 있지만, 반대 방향은 성립하지 않는데, 아주 중요한 차이가 됩니다. 모든 애님 그래프 노드는 이러한 이유 때문에 에디터 모듈에 존재하는 것이고, 게임과 함께 로드되지는 않고 에디터에서만 존재합니다. 반면에, 실제 블랜딩을 일으키는 비헤이비어 노드는 런타임 코드에 존재합니다. 반드시 여러분의 클래스가 올바른 모듈을 가르키도록 해야 됩니다.

스켈레탈 콘트롤 노드에도 동일 사항이 적용됩니다.

애님 그래프 노드

class UAnimGraphNode_ModifyBone : public UAnimGraphNode_SkeletalControlBase

애님 비헤이비어 노드

struct ENGINE_API FAnimNode_ModifyBone : public FAnimNode_SkeletalControlBase

스켈레탈 콘트롤 노드가 또한 다른 베이스 클래스를 가지고 있다는 것을 눈치채실 수 있을 것입니다.

이 시스템은 애님 그래프 노드가 노드 이름을 표시하는 등의 에디터 관련 작업을 할 수 있도록 디자인 되었으며, 툴팁이나 커스텀 핀을 보여줍니다. 애님 비헤이비어 노드는 실제 블랜딩과, 타겟 위치 계산, 올바른 최종 포즈 반환 등의 작업을 합니다. 그러므로 애님 그래프 노드는 에디터에서 중요한 반면, 애님 비헤이비어 노드는 런타임에서 중요하게 작용합니다.

문서 링크 를 참조하셔서 노드 변수의 입력과 반환에 관한 메타 데이터가 어떻게 바뀌는지를 읽어 보시기 바랍니다.

예를 들어, FPoseLink는 본 트랜스폼의 배열을 전달하는 포즈 링크인데, 아래처럼 정의할 수 있으면, 사진과 같이 나타나게 됩니다.

UPROPERTY(Category=Links)
FPoseLink BasePose;

FPoseLink가 어떻게 동작하는지를 알고 있는 것은 중요한데, 왜냐하면 여러분이 어떤 애니메이션 함수든 불러오면, 여러분은 포즈 함수를 불러야 합니다. 예를 들어, 여러분은 업데이트 함수를 가지고 BasePose->Update 를 콜해야 합니다. 이렇듯, 여러분은 BasePose를 멤버 변수로 가지고 있다면, BasePose->CacheBones을 여러분의 CacheBones 함수에서 콜해야 합니다.

여러분이 각 노드 타입에서 집중해서 보신 함수들에 관해 알아보도록 하겠습니다. 저는 애님 그래프 노드에는 촛점을 맞추지 않을 것인데, 왜냐하면 다른 블루프린트 노드와 매우 비슷하게 동작하기 때문이고, 저는 실제로 동작하는 노드를 집중적으로 보려고 합니다.

FAnimNode_Base를 살펴보도록 하죠

struct ENGINE_API FAnimNode_Base
{
    // Interface to implement
    virtual void Initialize(const FAnimationInitializeContext& Context) {}
    virtual void Update(const FAnimationUpdateContext& Context) {}
    virtual void Evaluate(FPoseContext& Output) { check(false); }
    virtual void CacheBones(const FAnimationCacheBonesContext& Context) {}
    virtual void GatherDebugData(FNodeDebugData& DebugData){}

};

이렇게 간단한 것은 아니지만, 여러분이 주로 보셔야 할 중요한 것들에 집중할 수 있도록 요약을 하도록 하겠습니다.

여러분의 노드가 어떻게 동작할 것인지를 결정하는 3개의 중요 함수가 있습니다. 이 것들은 Initialize, Update, Evaluate 이고 아래에는 이 함수들의 간단한 설명이 들어 있습니다.

  • Initialize - (메시 변경 등으로 인한)초기화/ 재초기화가 필요할 때 호출됨
  • Update - 현재 상황 업데이트시에 호출됨(플레이 타임 증가나 블렌드 위젯 업데이트 등의 상황에서) 이 함수는 업데이트시 현재 노드의 DeltaTime과 blend weight를 가지고 있는 FAnimationUpdateContext를 입력받습니다.
  • Evaluate - ‘포즈’를 만들어 내기 위해 호출됨(본 트랜스폼의 리스트)
    • 예제:
      • void FAnimNode_SequenceEvaluator::Evaluate (FPoseContext& Output)
        {
            if ((Sequence != NULL) && (Output.AnimInstance->CurrentSkeleton->IsCompatible(Sequence->GetSkeleton())))
            {
                Output.AnimInstance- >SequenceEvaluatePose(Sequence, Output.Pose, FAnimExtractContext(ExplicitTime));
            }
            else
            {
                Output.ResetToRefPose();
            }
        }

    • Evaluate는 시퀀스가 설정되었는지와 현재 스켈레톤과 호환되는지를 확인합니다. 조건이 맞는 경우 본 트랜스폼을 Output.Pose에 채워서 보내줍니다. 조건이 맞지 않는 경우 레퍼런스 포즈를 반환합니다.

 

이런 기본 함수들 위에, 여러분은 노드가 나머지 그래프와 잘 동작하도록 하는 두 개의 함수를 추가해서 확인해야 합니다.

virtual void CacheBones(const FAnimationCacheBonesContext& Context) {}
virtual void GatherDebugData(FNodeDebugData& DebugData){}

CacheBones은 노드에 의해 레퍼런스 된 본 인덱스들을 리프레시 하는 데에 쓰이고, GatherDebugData는 "ShowDebug Animation"의 데이터를 디버깅하는 데 쓰입니다. 이 함수들은 자손으로의 연결을 유지하는 데에 중요하게 사용됩니다. 제가 앞서서 말씀드렸듯, FPoseLink는 여러분의 노드에 연결된 어느 포즈 링크라도 호출될 수 있도록 하위에 연결된 모든 포즈를 호출해야 합니다.

이번 예제를 보시기 바랍니다:

void FAnimNode_BlendListBase::CacheBones(const FAnimationCacheBonesContext& Context)
{
    for(int32 ChildIndex=0; ChildIndex<BlendPose.Num(); ChildIndex++)
    {
        BlendPose[ChildIndex].CacheBones(Context);
    }
}

이 부분을 임플먼트해서 위 이벤트가 여러분의 노드에 의해 중지되지 않도록 합니다.

또한 FAnimationRuntime을 추가해서 애니메이션에 많은 기능을 제공합니다.

아래는 스켈레탈 컨트롤 노드의 예제입니다.

struct ENGINE_API FAnimNode_SkeletalControlBase : public FAnimNode_Base
{
    // FAnimNode_Base interface
    virtual void Initialize(const FAnimationInitializeContext& Context) override;
    virtual void CacheBones(const FAnimationCacheBonesContext& Context)  override;
    virtual void Update(const FAnimationUpdateContext& Context) override;
    virtual void EvaluateComponentSpace(FComponentSpacePoseContext& Output) override;
    // End of FAnimNode_Base interface
}

이 것은 애님 노드에 가깝지만 스켈레탈 컨트롤 노드가 컴포넌트 위치에서 동작하기 때문에 다릅니다. 다시 설명드리자면, FAnimationRuntime을 체크 아웃하는 것은 로컬 위치와 컴포넌트 위치를 변환하는 좋은 방법이 될 것입니다.

여러분은 EvaluateComponentSpace를 빈번하게 사용할 것입니다. 아래는 CopyBone 노드를 포함하는 경우입니다.

void FAnimNode_CopyBone::EvaluateBoneTransforms(USkeletalMeshComponent* SkelComp, FCSPose<FCompactPose>& MeshBases, TArray<FBoneTransform>& OutBoneTransforms)
{
    check(OutBoneTransforms.Num() == 0);
    // Pass through if we're not doing anything.
    if( !bCopyTranslation && !bCopyRotation && !bCopyScale )
    {
        return;
    }
    // Get component space transform for source and current bone.
    const FBoneContainer& BoneContainer = MeshBases.GetPose().GetBoneContainer();
    FCompactPoseBoneIndex TargetBoneIndex = TargetBone.GetCompactPoseIndex(BoneContainer);
    const FTransform& SourceBoneTM = MeshBases.GetComponentSpaceTransform (SourceBone.GetCompactPoseIndex(BoneContainer));
    FTransform CurrentBoneTM = MeshBases.GetComponentSpaceTransform(TargetBoneIndex);
    // Copy individual components
    if (bCopyTranslation)
    {
        CurrentBoneTM.SetTranslation( SourceBoneTM.GetTranslation() );
    }
    if (bCopyRotation)
    {
        CurrentBoneTM.SetRotation( SourceBoneTM.GetRotation() );
    }
    if (bCopyScale)
    {
        CurrentBoneTM.SetScale3D( SourceBoneTM.GetScale3D() );
    }
    // Output new transform for current bone.
    OutBoneTransforms.Add(FBoneTransform(TargetBoneIndex, CurrentBoneTM));
}

목적은 OutBoneTransforms를 원하는 데이터와 함께 반환하는 것입니다. 여러분은 원하는 만큼 많은 본 트랜스폼을 반환할 수 있으나, 계층구조에 주의하셔야 합니다. 부모에서 자식으로의 순서를 보존하는 것이 항상 안전합니다.

이 글이 여러분 스스로 커스텀 노드를 쉽게 만들 수 있도록 도움이 되었으면 합니다.