루트모션 (RootMotion)

언리얼엔진3 에 있는 루트모션에 대해 알아본다.

왜 필요한가?

루트모션은 캐릭터의 역동적인 좌표이동을 에니메이터가 직접 제어하게 하기위해 고안되었다.

기본 원리는 이러하다. 캐릭터의 최상위 루트 노드의 위치변화(translation delta)량을 캐릭터의 월드 좌표에 곧바로 반영한다.

일반적인 게임의 캐릭터 이동 로직은 고정의 acceleration 을 사용하여 시간의 흐름에 따라 선형적으로 velocity 를 증가시키고 그것이 캐릭터의 location 에 반영되는 구조이다. 이 구조에서는 acceleration 과 velocity 에 불규칙한 패턴을 주기가 어렵다. 하지만 루트모션을 이용하면 이것이 쉽게 가능해진다.

루트모션의 종류

엔진에서는 다양한 루트모션 방식이 존재하는데, 다양한 만큼 이들의 이용법이 약간씩 틀리다.

SkeletalMeshComponent.uc
enum ERootMotionMode
{
    RMM_Translate,   // move actor with root motion
    RMM_Velocity,    // extract magnitude from root motion, and limit max Actor velocity with it.
    RMM_Ignore,      // do nothing
    RMM_Accel,       // extract velocity from root motion and use it to derive acceleration of the Actor
    RMM_Relative,    // if bHardAttach is used, then affect relative location instead of location
};

중요한 것만 살펴보자면

RMM_Translate

acceleration 과 velocity 로 인한 좌표이동에 추가적으로 루트모션의 위치 변화량을 더해준다. 일반 이동과는 별개로 적용된다는 얘기

RMM_Velocity

루트모션의 위치 변화량1)으로부터 velocity 의 magnitude 를 구하고 그것을 속도한계치로 사용한다.

즉, acceleration 은 프로그래머가 직접 제어할 수 있지만2) velocity 는 magnitude 까지만 올라간다.

중요한(웃긴?) 것은 루트모션에서 magnitude 만 뽑아내고 direction 은 acceleration 의 것을 사용한다는 것이다. 이건 원치 않는 결과가 나올 수도 있는데 예를 들어 달리다가 180도 턴하는 동작이 있다고 치자. 이 동작의 루트모션 내용은 전진→정지→후진 정도가 될 것이다.3) 하지만 이 동작에 RMM_Velocity 를 적용하면 전진→정지→후진 의 magnitude 만 뽑아내고 acceleration 은 별도로 구하여 (사용자의 방향키 입력따위로부터) 적용한다. 즉 사용자가 전진키만 누르고 있다면 전진→정지→전진 이라는 엉뚱한 결과가 나온단 얘기. 좀 어이 없을진 모르지만 RMM_Velocity 의 진짜 용도는 따로 있으니 암튼 잘 알고 써야하겠다. 글고 방금의 예에서는 아래 설명할 RMM_Accel 을 사용해야 원하는 결과가 나온다.

RMM_Accel

루트모션의 위치 변화량으로부터 velocity 를 구하고 그것으로 다시 acceleration 을 구한다.

에니메이션 루트모션에 의해 velocity 와 acceleration 이 정해지기 때문에 프로그래머가 이를 직접 제어할 수 없다. (값을 직접 수정해도 소용없다.)

가장 많이 사용되는 모드이기도 하다.

루트모션을 수행하려면 그에 앞서 시퀀스의 RootBoneOption 에서 원하고자 하는 축의 옵션을 RBA_Translate 로 변경해 주어야 한다. 이것은 축별로 설정 가능하고 일반적으로 X,Y,Z 축 모두 RBA_Translate 로 변경하고 루트모션이 끝난 후에 다시 Default 값으로 롤백시켜준다.

RBA_Translate 옵션이 활성화 되야 루트본의 델타량 (이동/회전량) 을 별도의 변수에 추출하여 저장해두고 그 값을 사용하여 루트모션을 수행하기 때문이다. 더 자세한 사항은 UAnimNodeSequence::ExtractRootMotion 을 참고

차이점

위 세가지 루트모션은 추구하는 바는 같지만 그 과정이 같지가 않다. 이로인해 약간 쓰임새가 다른데 각각의 적절한 예를 들어보면 다음과 같다.

  • RMM_Translate - acceleration 과 velocity 와는 별도로 (추가로) 좌표이동을 적용하기 위해 사용된다. 보통은 사용할 일이 별로 없다. In-Game 물리가 적용되지 않는다. (예를 들어 eventBump 라던가)
  • RMM_Velocity - 루트모션은 오로지 그 프레임에서의 최대 velocity 한계치 를 의미한다. 그리고 위에 설명한 대로 루트모션으로부터 방향은 추출하지 않는다. accel 을 아무리 밟아대도 캐릭터의 velocity 는 한계치까지만 올라간다. 이 모드의 장점은 acceleration 이 자유롭다는 것이고 단점은 위에 설명한 대로 어이없는 결과가 나올 수 있다는 것이다. 암튼 정리하면 acceleration 은 전적으로 프로그래머가 제어하지만 여기선 방향성만 취하고4) 여기에 velocity 는 한계치만큼만 올릴 수 있다. In-Game 물리가 적용된다.
  • RMM_Accel - velocity 와 acceleration 이 전적으로 루트모션에 의해 정해지기 때문에 프로그래머가 신경쓸 것이 없다. 하지만 이것은 프로그래머가 제어할 수 있는게 없다는 뜻이기도 하다. 가장 일반적으로 사용된다. In-Game 물리가 적용된다.

코드

  • RMM_Translate
    UnSkeletalComponent.cpp
    void USkeletalMeshComponent::ProcessRootMotion( FLOAT DeltaTime, FBoneAtom& ExtractedRootMotionDelta, INT& bHasRootMotion )
    {
        ...
        if ( bHasRootMotion )
        {
            ...
            // 이번 tick 에 in-game physics 가 적용되었기 때문에 다음 프레임에 루트모션을 적용하기 위해 PreviousRMM 과 비교한다.
            const UBOOL  bCanDoTranslation = ( RootMotionMode == RMM_Translate && PreviousRMM == RMM_Translate );
     
            // 이하 코드 참조
            ...
        }
        ...
    }
  • RMM_Velocity 와 RMM_Accel
    UnPhysic.cpp
    void APawn::CalcVelocity( FVector &AccelDir, FLOAT DeltaTime, FLOAT MaxSpeed, FLOAT Friction, INT bFluid, INT bBrake, INT bBuoyant )
    {
        ...
    }

응용

방향 키 입력을 받아 루트모션을 행하려는 경우

개발을 하다보면 방향키 입력을 받는 동안 루트모션이 있는 에니메이션을 반복적으로 재생할 필요가 생길 것이다.

예를 들어, 전사가 방패를 들고 가드를 올린 상태에서 앞쪽방향키를 입력하면 특정 템포를 가지고 전진한다던지 (이 경우 선형 운동이 아니라 템포가 있는 전진이기 때문에 루트모션으로 제작되는 것이 더 바람직하다.) 아니면 GoW2 의 경우 DBNO 에 빠진 병사가 방향키를 누르면 비틀거리며 해당 방향으로 기어가는 경우가 그러하다.

이와 같이 '방향키가 눌렸을 경우 루트모션이 수행되는 경우' 라면 RMM_Velocity 와 AnimTree 의 노드분기를 사용하는 것이 좋다. 이유는 아래와 같다.

  • 방향키를 누르면 특정 행위를 반복적으로 수행하는 것이기 때문에 SM 보다는 AnimTree 의 노드분기를 사용하는 것이 더 좋다. (편하다)
  • 방향키를 누른다는 것은 이동을 한다는 것이고 그것은 velocity 계산에 영향을 미친다. 만약 RMM_Accel 을 사용하면 사용자의 키 입력에 따른 Acceleration 을 사용하지 않고, 루트모션의 Velocity 를 추출하여 Actor.Velocity 와 Actor.Acceleration 을 구하는데 (Pawn::CalcVelocity 참조) 문제는 루트모션이 있는 에니메이션은 재생조차 되지 않는 상황이라 결국엔 키 입력에도 불구하고 아무런 반응을 하지 않게 된다. 때문에 RMM_Accel 은 안된다.
  • 마지막으로 방향성의 독립이 보장되는 것은 RMM_Velocity 밖에 없기 때문이다. RMM_Velocity 만이 acceleration 이 독립적이고 방향을 위해 사용될 수 있기에 위이 예에 적절하다. 가드포즈나 DBNO 모두 4방향 blend 가 되어야 하고 AnimTree 에서 AnimNodeBlendDirectional 정도가 사용될텐데 이 때 blend 를 Acceleration 으로 이루어지게 하고 RMM_Velocity 를 사용하여 이동하게 하면 무난하게 원하는 것을 구현할 수 있겠다.

RMM_Velocity 를 사용하고 AnimTree 의 노드분기가 Acceleration 을 기준으로 분기가 이뤄지게 한다면 베스트다. 다만 Autonomous 와 Authority 는 정확한 Acceleration 값을 가지지만 Simulated 의 Acceleration 은 단지 dir 역할만 한다. (노멀라이징 된다.)

참고

1) USkeletalMeshComponent.RootMotionDelta 에 저장되며 USkeletalMeshComponent::ProcessRootMotion 함수에서 매 틱마다 구한다.
2) 따라서 RMM_Velocity 는 Pawn.bForceMaxAccel 과 조합하여 사용 가능하다.
3) 방향을 바꾸는 것은 논외로 치고
4) acceleration 의 magnitude 는 의미 없다.