
저자: Shadow
【USparkle 칼럼】 뛰어난 재주를 품고, “연구 좀 해보는 것”을 사랑하며, 공유를 즐기고 남의 장점도 널리 받아들이는 당신을 기다립니다. 지혜의 불꽃이 부딪히고 어우러지고, 지식의 전달이 끊이지 않고 이어지도록!
이 글은 USparkle(侑虎科技)의 2016번째 글입니다. 저자 Shadow가 원고를 제공해 주셨습니다.
머리말
UE: 5.7.1
GASP: 5.7
본 글은 Mover 버전 캐릭터를 기준으로 GASP의 회전 구현을 가볍게 분석한다. Strafe 이동, AimOffset, TurnInPlace를 다룬다.
또한 Lyra와 ALS의 일부 구현 방안도 함께 살펴본다.
1. Debug와 기초 RotationMode
1.1 Debug

회전 관련 Debug 정보를 더 편하게 보기 위해 Widget에서 Draw Shapes를 켜야 한다. 그러면 아래 그림처럼 수많은 화살표가 보이는데, 그중 의미 있는 부분은 다음과 같다:
- 원 안쪽의 검은 큰 화살표: Root 루트 본 방향
- 원 바깥의 검은 작은 화살표: 캐릭터(콜리전) 방향
- 밝은 분홍색 화살표: TargetOrientation 회전 오프셋 적용 전의 목표 방향
- 어두운 분홍색 화살표: TargetOrientation+RotationOffset 회전 오프셋 적용 후의 목표 방향
- 주황색 화살표: MovementIntent, 캐릭터가 원하는 운동 방향
- 녹색 호선: 현재 MovementDirection 이동 방향의 범위

1.2 RotationMode
GASP에서 회전 모드는 OrientToMovement, Strafe, Aim으로 나뉜다. ALS를 써본 사람이라면 이것들이 VelocityDirection, LookingDirection, Aiming과 일대일로 대응된다는 것을 알 것이다.
- OrientToMovement: 캐릭터가 이동 방향을 바라보도록 회전한다
- Strafe: 캐릭터가 이동할 때 대체로 조준 방향을 바라보며 회전한다
- Aim: 캐릭터가 항상 최소 오프셋으로 컨트롤러 회전 방향을 정확히 바라본다. 이것도 Strafe 모드라고 할 수 있다
RotationMode는 주로 플레이어의 키 입력과 게임 모드에 의해 결정된다. GASP에서는 마우스 가운데 버튼으로 Strafe와 Movement 두 모드를 전환할 수 있고, 우클릭을 누르고 있으면 Aiming 모드로 들어간다. 흔히 보는 오픈월드 게임은 대부분 기본으로 OrientToMovement 모드를 쓰고, 플레이어가 활이나 총을 들고 조준할 때 Strafe로 들어간다.

이 중 OrientToMovement는 비교적 간단하다. 캐릭터가 어느 쪽으로 움직이든 그쪽으로 회전하면 되므로, 더 자세히 소개하지는 않는다
2. Strafe 기초
2.1 BlendSpace 방식의 문제점
내가 Strafe 이동을 처음 접한 것은 UE4 초보 튜토리얼을 따라 전투 시스템을 만들 때였던 것으로 기억한다. 활 조준이나 세키로(只狼)의 타겟 락온 같은 것을 만들 때쯤이 되면, 플레이어가 항상 어느 한 방향을 바라보면서도 다른 방향으로 이동하는 상황이 필요해진다. 하지만 초보 튜토리얼의 방식은 대개 매우 단순해서, 예를 들어 아래 그림처럼 Direction 하나로 바라보는 방향과 얼마나 어긋났는지 각도를 표현한 뒤 1D BlendTree로 블렌딩하는 식이다. 아주 조금 더 나은 경우에는 2D BlendTree를 써서 LR/FB로 블렌딩하기도 한다.

1D는 링(ring) 형태가 아니기 때문에, 좌후방에서 우후방으로 전환해야 할 때 -180도에서 180도까지 중간 상태를 전부 지나가야 해서 움직임이 매우 이상해진다. 2D도 사방향이든 블렌딩되는 부분에서는 예상 밖의 움직임이 나올 수 있다. 블렌딩 움직임이 좋지 못한 곳에는 팔방향 애니메이션을 보완하는 방법을 선택할 수 있다.
RootMotion이라면 블렌딩 후 이동 속도가 느려지는 문제도 고려해야 한다. 예를 들어 전방향과 좌방향 이동 속도가 모두 100인데, 좌전방으로 갈 때는 속도가 밖에 안 된다. 하지만 우리가 기대하는 것은 당연히 대각선 이동도 100이다. 가장 간단한 방법은 애니메이션 재생 속도를 조절해 속도를 맞추는 것이지만, 다소 괴랄해진다. 프로그램으로 보상해서 스케일하는 것도 생각할 수 있고, 아예 프로그램으로 직접 이동시킬 수도 있다.
2.2 프로시저럴 다방향 애니메이션
2D BlendTree라고 해서 꼭 네 방향 애니메이션으로 고정할 필요는 없다. 필요에 따라 8방향이든 10방향이든 유연하게 조정해도 OK다. GASP는 F, B, LL, RL 네 개의 애니메이션을 사용했지만, GASP는 BlendTree로 블렌딩해 만든 것이 아니다. BlendTree 융합은 애니메이션이 없는 각도에서 융합된 캐릭터가 기대에 못 미치는 경우가 많은데, GASP가 사용한 것은 Orientation Warping이다. 이것도 마찬가지로 프로시저럴한 수정이지만 작은 범위에서는 효과가 괜찮다. Lyra는 더더욱 과감하게(무식하게) 애니메이션 하나만으로 OrientationWarping을 통해 360°를 뽑아냈다.
OrientationWarping은 발이 운동 방향에 따라 서로 다른 방향으로 움직이게 할 수 있다. 하반신 움직임은 이동 방향에 맞추고 상반신은 그대로 유지해서, 다양한 각도로 Strafe를 구현한다. 구현 로직은 먼저 Root를 회전시키고, 그다음 spine을 반대로 순차적으로 회전시키는 것이다. 예를 들어 아래 그림은 정우향을 사용했지만 45도 warp되어 우전방이 된 경우다.

2.3 다리 관통
BlendSpace든 OrientationWarping이든 이런 방법들은 발 교차 문제를 해결할 수 없다. 발 꼬임은 교차된 발 상태에서 다른 Pose로 융합될 때 자주 나타나는데, 전환되는 쪽의 발 전후가 반대라서 그대로 융합해 버리면 관통이 생긴다. 예를 들어 아래 그림은 원래 오른쪽으로 걷는 왼발이 앞에 있었는데, 교차 시 뒤로 전환되면서 오른발이 앞에 오게 되어 다리가 관통한다.

이 문제를 해결하기 위한 비교적 흔한 방법은 섹터 분할이다. 전방향과 후방향을 각각 다른 blendTree로 만들고, 각 섹터 안에서는 항상 같은 발이 앞에 오게 하며, 같은 재생 진행도에서는 발이 동일한 전후 위치에 있도록 보장한다. 이렇게 하면 섹터 내에서 Blend할 때 앞다리가 뒤로 가야 하는 관통 상황이 발생하지 않는다. 동시에 경계에서 섹터가 빈번하게 전환되는 것을 피하기 위해 deadZone도 추가해 각 섹터의 범위를 조금씩 넓혀 겹치는 영역을 둔다.

융합이 발생할 때 융합 전후의 Pose에서 한쪽은 발이 앞에 있고 다른 쪽은 발이 뒤에 있으면 융합 시 관통 문제가 나타난다. 하지만 사실 이것을 고치는 것도 매우 간단하다. 적절한 타이밍에 융합해 주기만 해도 해결된다. 같은 발이 앞에 있거나, 교차되지 않은 상태일 때 하면 된다. 아래 영상이 보여 주는 것과 같다.

어떤 시간 노드에 도달해야만 전환을 허용하도록 고를 수도 있지만, 이러면 조작감에 영향을 준다. 더 좋은 방법은 SyncGroup을 사용해 전환되는 타이밍을 서로 맞추는 것이다.
그 외에도 연결 애니메이션 방식으로 해결할 수 있다. 전환 애니메이션이 애니메이션 직접 융합을 대신해 발 꼬임 상황을 회피하는 것이다. 이렇게 하면 전환 과정에서 여전히 두 애니메이션이 직접 융합되어 움직임이 기대에 어긋날 수 있는 문제도 개선할 수 있다
3. GASP의 Strafe
3.1 MovementDirection 섹터 분할
GASP에서도 발 꼬임 관통 문제를 피하기 위해 마찬가지로 섹터 분할 방식을 사용했다.
최신 프로젝트에서는 실험적인 기능도 열어 두었다. Aim과 Strafe에 대해 각각 다른 섹터 스타일을 선택할 수 있는데, 전후좌우 네 개의 섹터로 나눌 수도 있고, 두 개 또는 하나만 둘 수도 있다.

하지만 열거형을 열어 보면 F B L R 네 개만 있는 것이 아니라 F B LL LR RR RL 여섯 개라는 것을 알 수 있다. ALS를 본 적이 있다면 굉장히 익숙하게 느껴질 것이다. ALS의 RF RB와 비슷한데, RF는 캐릭터가 오른쪽으로 운동하지만 가슴이 오른쪽을 향한다는 뜻이고, RB는 캐릭터가 오른쪽으로 이동하지만 가슴이 왼쪽을 향한다는(등이 오른쪽) 뜻이다. GASP에서도 마찬가지로 첫 번째 글자 L/R은 어느 쪽으로 이동하는지를, 두 번째 L/R은 가슴의 방향을 나타낸다. 예를 들어 RL은 오른쪽으로 이동하고 등이 왼쪽을 향한다는(가슴이 오른쪽) 뜻이다. 아래 그림과 같다.

MovementDirection의 선택도 매우 간단하다. 현재 운동 방향이 어느 범위에 있는지에 따라 그것을 선택하면 된다. 하지만 좌우 가슴 방향이 다른 것을 전부 쓴 것은 아니다. 현재 Mover 버전의 구현에서는 LL을 L로, RL을 R로 채용해 F B L R 네 부분을 구성했다. (다만 애니메이션을 보면 RR과 LR도 예비로 남겨 두어서, 이후 ALS처럼 6방향으로 확장할 가능성이 있다)
FL FR BL BR 네 개의 각도 값으로 네 개의 섹터를 구분할 수 있다. 아래 그림과 같이, 각 섹터에 있을 때마다 현재 섹터 범위를 살짝 넓혀 DeadZone을 형성해서 경계에서 미친 듯이 왔다 갔다 하는 것을 방지한다.


Debug 모드의 초록색 원은 현재 섹터의 범위를 나타낸다. 우리가 선택한 Strafe Style이 다르면 섹터가 덮는 영역이 달라지면서 섹터 수 제어가 구현된다. 예를 들어 F B 모드에서는 FL과 BL이 같은 값이므로 L 섹터에 진입하는 경우가 존재하지 않는다.

3.2 Rotation Offset

Mover 캐릭터를 조작해 이동할 때 Debug를 켜 두면 어두운 보라색 화살표가 하나 자주 눈에 띄고, 캐릭터의 방향이 그것을 따라간다는 것을 발견할 수 있다. 실제로 우리가 원하는 방향(밝은 보라색 화살표)에서 오프셋된 것이다. 이것이 RotationOffset이며, ALS의 Yaw Offset과 같은 것이다.
계산은 아주 간단하다. 현재 이동 방향과 기대 방향의 사잇각으로 커브를 샘플링하는 것이다. 예를 들어 위 그림의 경우, 밝은 보라색 기대 방향 TargetOrientation과 주황색 운동 방향의 사잇각이 +135°이고, 이때 MovementDirection이 B이면, Curve_RotationOffset_B를 샘플링해 -45의 RotationOffset을 얻는다.

RotationOffset을 끄면 같은 섹터 내에서는 애니메이션 차이가 크지 않지만, 섹터를 전환할 때 연결 문제가 생길 수 있다. 캐릭터의 방향이 계속 앞을 향하면 잘못된 연결 애니메이션이 선택되기 때문인데, 이것은 뒤에서 이야기하겠다.
(여기에 작은 디테일이 하나 있는데, GASP의 OrientationWarp에는 AngleThreshold가 135로 열려 있다. 이것을 테스트하려면 좀 더 크게 키워야 한다. 이 값은 본질적으로 Orientation 과도를 방지하기 위한 것으로, 그렇지 않으면 135도를 넘었을 때 반전되어 등 방향으로 45도 Warp되는 오류가 생긴다)
RotationOffset의 역할은 주로 캐릭터 애니메이션을 더 자연스럽게 하는 것이다. 아래 그림에서 하나는 Offset이 있고 하나는 없는데, 같은 프레임을 캡처하지는 못했지만 애니메이션이 약간 다르다는 것을 알 수 있다. RotationOffset이 있으면 캐릭터 이동 방향과 방향의 차이가 180°로 마침 원래 애니메이션과 같아져서, OrientationOffset으로 프로시저럴하게 애니메이션을 수정할 필요가 없다. 아무래도 애니메이터가 수작업으로 키를 찍은(K한) 애니메이션이 융합이나 프로시저럴하게 수정된 것보다 움직임이 좋을 확률이 높다. ALS에서 YawOffset은 다방향 융합을 피하고 최대한 원래 애니메이션을 쓰기 위한 것이다. 둘 다 최대한 더 자연스러운 원래 애니메이션을 사용하기 위함이다.

하지만 이렇다면 왜 이 RotationOffset을 계속 유지하지 않는 걸까? 그러면 BlendTree 융합이나 프로시저럴 수정 없이 계속 원본 애니메이션을 쓸 수 있을 텐데. 아래 그림에서 볼 수 있듯 GASP에서 오른쪽일 때, 이동 방향이 우하향이면 45° 오프셋이 있지만, 우상향을 바라볼 때는 오프셋이 우리가 기대한 -45°가 아니라 0이다. 왼쪽도 마찬가지다.

억지로 고쳐서 좌우 섹터 모두 RotationOffset의 오프셋을 갖게 하면 아래 영상의 효과를 볼 수 있다. AimOffset이 존재하기 때문에 발 동작은 정상이지만, 이때 가슴과 머리는 정면을 향하고, 그러면서 캐릭터 방향은 대각선으로 이동해서 움직임이 다소 이상해진다. 또한 이때는 작은 각도 45°이므로 OrientationWarp도 그다지 왜곡되지 않는다. 그래서 이때 GASP는 Offset이 아니라 OrientationWarp로 이 상황을 처리하기로 선택한 것이다.

3.3 연결 애니메이션 리소스
섹터 간 전환에 대해 GASP는 전환 애니메이션 해법을 채택해, MotionMatching과 Chooser에 의존해 대량의 연결 애니메이션을 만들었다.

GASP에는 이런 연결 애니메이션이 대량으로 있다. Run을 예로 들면 위 그림처럼 빽빽할 정도로 많고, 매우 디테일하게 만들어져 있다. 좌우 발을 구분했을 뿐 아니라 다양한 타입도 있다. 이 box, spin 같은 것들은 무용의 발놀림 전문 용어인 것 같지만, 우리는 궤적과 작용으로 구분할 수 있다.
Spin: 운동 방향이 변하지 않는 상황에서 캐릭터 방향을 변경
Box: 방향을 유지한 채 운동 방향이 90도 변경, 궤적은 L
Pivot: 방향을 유지한 채 운동 방향이 180도 변경
Turn: 방향과 운동이 동시에 변경되며, 이동 방향과 캐릭터 방향이 항상 일치
Switch: 운동 방향과 방향 모두 변하지 않지만, 가슴 방향이 좌우로 전환
아래 그림은 각 타입별로 예시를 하나씩 든 것이다.

3.4 연결 애니메이션 선택
이렇게 많은 리소스가 각기 다른 기능을 갖고 있는데, GASP에서는 어떻게 골라 사용할까?

GASP에서는 Chooser로 일련의 적합한 애니메이션을 고른 뒤, 다시 MotionMatch로 선택한다.
선택 조건 중에는 StateMachineState가 Transition to Locomotion이어야 선택된다. 이 상태의 진입 조건은 주로 두 부류인데, 하나는 현재 상태가 변화하는 경우, 가장 핵심적인 것이 movementdirection이고, 다른 하나는 재생 중인데 빠르게 조정이 생겨 현재 애니메이션이 맞지 않게 되면, 다시 진입해 적합한 애니메이션 선택을 다시 트리거하는 경우다.
하지만 StateMachineState는 이 많은 애니메이션을 구분하는 근거가 될 수 없고, 주로 뒤의 세 가지에 의존한다. FutureFacingDelta, IsPivoting, MovementDirectionRecent다. 이 중 FutureFacingDelta와 IsPivoting은 모두 미래 궤적으로 예측된 각도에 기반해 계산한다.

FutureFacingDelta는 현재 Root 방향과 기대 방향의 차이로 계산되며, 캐릭터의 방향이 미래에 변화하는 각도를 계산하는 데 쓰인다. 즉 Debug 모드의 어두운 분홍색 화살표의 변화다. 이 파라미터로 캐릭터 방향이 변화하는 Spin, Turn과 변하지 않는 Switch, Box, Pivot을 구분할 수 있다. 동시에 이 파라미터로 Turn의 각도, 회전의 좌우, 어떤 spin 애니메이션을 쓸지도 구분할 수 있다. 예를 들어 LL 상황에서 F로 돌아오면 왼쪽으로 90도 회전인데, 표에서 이 애니메이션의 값 범위는 -110에서 10도로 딱 맞으면서도 +-20의 공차가 더해져 있다.
IsPivoting은 운동 방향이 변화했는지 판단하는 데 쓰인다. 미래 궤적과 현재 운동 방향으로 방향 사잇각을 하나 계산해서, 그것이 Ground에서는 75도보다 크고 일정한 이동 속도가 있으면 True가 된다. 이것을 사용하면 Spin, Switch 같은 운동 방향이 변하지 않는 애니메이션을 다른 것과 구분할 수 있다.

맨 처음에 MovementDirection을 소개했지만, 이 Recent가 붙은 파라미터와는 다소 다르다. 예를 들어 F 방향으로 전환할 때, 먼저 MovementDirection으로 현재 방향을 선택하고, 그다음 그 안에서 어느 방향에서 돌아왔는지를 선택한다. 방금의 FutureFacingDelta가 몇 도 회전인지를 기술하는 것 외에, MovementDirectionRecent는 이전의 안정된 상태가 무엇이었는지를 나타내는 데 쓰인다. 이 파라미터는 현재 MovementDirection이 0.1초 이상 유지되어야만 저장·갱신된다.
이렇게 하면 빠르게 전환하는 일부 경우에, 캐릭터 방향과 운동 방향이 사실 모두 변하지 않았을 때, Recent 파라미터가 애니메이션이 선택되지 않게 해서 연결 애니메이션을 건너뛰고 바로 원래 Loop로 돌아가게 한다. 연결 애니메이션을 재생했지만 실제로는 거기서 연결해 올 필요가 없었던 상황을 피하는 것이다. 동시에 Switch 애니메이션에 선택 근거도 제공한다. 방향이 모두 변하지 않았기 때문에 Recent를 통해서만 선택할 수 있기 때문이다.

그래서 정리하면, 앞서 언급한 각 애니메이션 타입의 모식도에 대해.
FutureFacingDelta는 캐릭터 방향 변화를 결정한다. 즉 보라색 화살표의 정보다
IsPivoting은 운동 방향이 변화했는지를 결정한다. 즉 주황색 화살표의 정보다
MovementDirectionRecent는 둘 다 변하지 않았을 때 어떻게 선택할지를 결정한다. 즉 가슴 방향을 구분하는, 녹색 화살표 정보다
세 파라미터가 함께 작용해 애니메이션 DataBase 선택 문제를 해결한다.
마지막으로 다시 MotionMatching에 따라 L/R Foot을 매칭하고, 미래 궤적으로 운동 궤적을 결정해 애니메이션 라이브러리에서 최종 선택을 완료한다.
4. AimOffset

AimOffset은 비교적 관례적인 방법이다. 수많은 Pose로 BlendSpaces를 하나 구성하고, Yaw와 Pitch로 샘플링한다. 마지막으로 MeshSpaceAddtive로 위에 얹는다. 왜 MeshSpace인지는 공식 문서의 설명을 보면 된다 https://dev.epicgames.com/documentation/zh-cn/unreal-engine/aim-offset-in-unreal-engine
Aim Offset in Unreal Engine | Unreal Engine 5.8 Documentation | Epic Developer Community
An Aim Offset is a type of Blend Space that uses additive poses, typically for creating aim-spaces.
dev.epicgames.com

하지만 AO에는 주목할 만한 디테일이 하나 있다. GASP의 AO에도 스무딩이 있지만, 최종 계산이 끝난 Yaw와 Pitch를 스무딩하는 것이 아니다. 그렇게 하면 이때 턴 애니메이션이 재생되면 아래 영상처럼 되기 때문이다(빨간 화살표는 Head 방향). AO는 Root와 Aiming 방향의 보간인데, Root 회전이 빠른데 AO가 천천히 스무딩되면 목표에서 벗어나, 머리가 턴 과정에서 조금 더 삐딱해졌다가 다시 바로잡힌다.

해결도 간단하다. Aiming 방향을 스무딩하되, Root와 Aiming의 차이 계산은 스무딩하지 않으면 된다. 이렇게 하면 Root를 따라갈 수 있어 스무딩으로 인한 잘못된 오프셋을 피할 수 있다.
5. 제자리 회전
5.1 ALS와 Lyra의 제자리 회전 회고
Turn이 없으면, 캐릭터 회전은 계속 카메라를 따라 아래 영상처럼 그냥 전체가 회전하게 되어 정상적인 생물의 움직임과 매우 동떨어진다. 계속 회전하지 않고 AO에 의존해 목표를 바라보게 할 수도 있지만, 목표 지점이 뒤에 있으면 사람도 머리를 180° 돌려 뒤를 볼 수 없다. 일반적으로 AO의 한계도 90° 정도다. 현실을 생각해 보면 우리가 다른 방향을 보려다 일정 각도에 도달했을 때, 발을 움직여 몸을 돌린다. 이것이 TurnInPlace다.

GASP가 어떻게 하는지 보기 전에, ALS와 Lyra의 흐름을 되돌아보자.
ALS의 캐릭터 회전 방향은 CMC에 내장된 ControllerDesiredRotation이나 RotationToMovement를 사용하지 않았다. 그래서 이동할 때 기대 방향에 따라 회전할 수 있고, 제자리에서는 특별한 로직이 없으면 Actor가 회전하지 않는 상태를 유지한다.
ALS가 Strafe 또는 Aiming 모드에 있을 때, Control과 Actor의 방향 사잇각을 계산한다. 일정 각도를 넘으면(Strafe는 일정 시간 유지도 요구한다) 방향과 각도에 따라 적합한 애니메이션을 골라 재생한다. 매 프레임의 회전량은 애니메이션 커브에 저장되고, 실제 회전 각도에 따라 애니메이션 재생 속도를 조정하고 회전 커브 값을 스케일해서 하나의 애니메이션이 여러 다른 각도에 대응하도록 구현한다. 매 프레임 이 회전 커브 값을 적용하면 제자리 회전이 구현된다.
하지만 이렇게 하면 문제가 하나 있다. 게임 프레임레이트가 매우 낮으면 일부 커브 포인트를 건너뛰어 회전이 충분히 적용되지 않는다. ALS는 그냥 억지로 몇 배 시간이 지났으면 몇 배로 스케일하는데, 이러면 최종적으로 약간의 편차가 생긴다.

Lyra는 비교적 간단하다. 우선 Lyra의 Actor 회전은 항상 ControlRotation을 따르기 때문에 Mesh도 따라 움직인다. Mesh를 움직이지 않게 하기 위해, Lyra는 ABP에서 차이 값을 하나 계산해 반대로 Root에 적용해 그대로 유지시킨다.
회전 애니메이션에서 Lyra는 턴 애니메이션이 다른 각도에 적응하는 것을 고려하지 않았고, 과감하게(무식하게) 그냥 90도 턴 애니메이션을 직접 재생한 뒤, 턴 후에 AO로 목표에 정렬한다.

5.2 Distance Match For Turn In Place
(정정: 여기서 언급한 AdvanceTime은 UE의 노드와 조금 다르다. UE 노드는 Delta를 입력받아 그 Delta에 따라 전진하고, 남은 시간을 추격하지 않는다. 하지만 여기서는 AdvanceTime을 Remain 입력처럼 직접 다뤘는데, RateLimit이 달린 ToTarget에 더 가깝다)
이 방안은 UE 공식이 Paragon Feature Examples와 Bringing a Hero from Paragon to Life with UE4 공유에서 언급한 것이다(링크는 말미에). DistanceMatch는 우리가 잘 아는 Lyra에서 정지 스텝의 거리 매칭에 쓰였다. 이것은 이동 거리뿐 아니라 회전에도 쓸 수 있다. DistanceMatch로 회전을 할 때, 우리는 원하는 회전 각도와 회전 커브에 따라 적합한 시간 포인트를 매칭할 수 있다. 시간 기반의 선형 재생으로 구동하는 것이 아니다
Lyra는 스텝 시작·정지 애니메이션에서 DistanceMatching이라는 두 개의 노드를 사용했다.

DistanceMatching의 본질은 애니메이션 재생의 시간 포인트를 조정하는 것이다. DistanceMatchToTarget을 쓰면 적합한 위치로 바로 점프해 재생한다. 예를 들어 멈출 때 앞부분 애니메이션이 재생되지 않고 뒤에서 바로 시작할 수 있다. 다른 하나인 AdvanceTime은 PlayRateClamp를 제거하면 사실 ToTarget과 같지만, PlayRateClamp가 있기 때문에 애니메이션 재생 속도를 스케일한다고 이해할 수도 있다. 하지만 단순히 PlayRate를 스케일하는 것과 비교하면, 실제 운동의 리듬과 애니메이션의 리듬이 반드시 맞지는 않는다. 아래 그림을 보라. 재생 속도만 스케일하면 최종 시간은 맞지만, 과정에서 실제 변위와 맞지 않아 발이 미끄러진다. 아래 그림은 비교적 간단한 경우를 보여 준다. 애니메이션 변위와 실제 변위가 일치할 때, PlayRate 스케일과 DistanceMatch 사용을 비교한 것이다. 애니메이션 변위가 너무 크면 속도를 늦춰 기다리고, 애니메이션이 너무 느리면 가속 재생해 따라잡는다.

하지만 실제 사용 과정에서는 실제 변위와 애니메이션 변위가 같은 경우가 나오지 않는다. 아래 그림에서 왼쪽은 애니메이션 변위가 실제 변위보다 큰 경우, 오른쪽은 애니메이션 변위가 실제 변위보다 작은 경우다. PlayRateClamp의 제한이 있기 때문에, 최종적으로 애니메이션 변위와 실제 변위에 격차가 생겨 발이 미끄러진다
애니메이션 변위가 작을 때, ToTarget을 쓰면 앞부분 애니메이션이 유실되고, AdvanceTime은 먼저 느린 속도로 재생해 실제 남은 변위가 애니메이션 남은 변위를 따라잡게 한다.
애니메이션 변위가 클 때, ToTarget을 쓰면 앞부분 애니메이션이 유실되고, AdvanceTime은 먼저 애니메이션을 가속해 애니메이션의 남은 변위를 줄여 실제 남은 변위를 따라잡는다.

위에서는 모두 거리를 이야기했지만, 거리를 회전 각도로 바꿔 이해해도 마찬가지다. 볼 수 있듯 AdvanceTime의 궤적에는 항상 실제 변위와 맞지 않는 구간이 있어 발 미끄러짐 문제가 생긴다. ToTarget은 비교적 잘 매칭되지만, 대가로 일부 애니메이션을 유실한다.
그래서 Turn에 쓸 때는 스텝 시작 애니메이션과 비슷하게, 처음부터 재생하면서 해당 각도의 회전에 대응하기를 바라므로, 우리는 AdvanceTime을 사용한다. 하지만 애니메이션이 너무 괴랄해지는 것을 피하기 위해 PlayRate 제한이 있다. 여전히 어떤 순간에는 실제 회전과 편차가 생겨 발이 미끄러진다. 이것은 FootLock과 함께 해결해야 한다. 그래서 DistanceMatch는 PlayRate 범위 안이라면 턴이 끝날 때 애니메이션도 딱 끝나도록 해서, 애니메이션이 로직 회전에 매칭되도록 해 줄 뿐이다.
5.3 TurnInPlace In GASP
ALS, Lyra, 그리고 Paragon의 방안을 살펴봤으니, 다시 GASP로 돌아와 보자.
우선 GASP는 Aiming 상태에서만 TurnInPlace 움직임이 있다. Mover 컴포넌트 아래에서, 캐릭터의 방향은 입력된 OrientationIntent에 의해 결정된다. Aiming 제자리에서는 60°를 넘어야 OrientationIntent가 AimingRotation 방향으로 갱신된다. 이후 FacingSmoothingTime이라는 파라미터로 스무싱해 간다. 이 각도 제한을 제거하면 CMC의 UseControllerDesiredRotation과 같아져, 캐릭터 회전이 계속 컨트롤러 방향으로 스무스하게 회전한다.

이후 Root와 기대 방향의 차이로 앞서 말한 FutureFacingDelta를 계산하고, 이 값이 50°를 넘으면 shouldTurnInPlace를 트리거해 DataBase를 제자리 Turn 애니메이션으로 전환해 Turn을 트리거한다.

여기서 Aiming 모드에서는 60°가 되어야 캐릭터의 기대 방향이 수정되고, 일단 바뀌면 60>50°이므로 TurnInPlace가 트리거된다. 하지만 TurnInPlace를 트리거하는 각도를 키우면 어떻게 될까? 90°로 바꾸면 아래 영상의 상황을 볼 수 있다(OffsetRoot는 끈 상태). 아직 TurnInPlace가 트리거되지 않았지만 캐릭터는 회전해 있다. 마지막처럼 마우스를 휙 움직여 한 번에 90°를 넘기지 않는 한 영원히 Turn을 트리거할 수 없고, 그냥 미끄러져 간다.

하지만 이것은 올바르지 않다. Turn 트리거 각도에 도달하지 않았을 때, 올바른 것은 제자리에서 회전하지 않고 유지하는 것이다. 하지만 캡슐이 이미 회전했기 때문에, Mesh가 회전하지 않게 하려면 Root를 반대로 계산해 돌려야 한다. 이것이 Lyra의 방법이다. GASP는 새로운 노드를 채용해 구현해서, 블루프린트에서 온갖 계산을 한 뒤 마지막에 ModifierBone 노드로 Root를 수정해야 하는 수고를 피했다.

OffsetRootBone 노드에서 RotationMode는 Montage 재생 시에만 Release 모드를 쓰고, 다른 경우에는 모두 Accumulate다. 이 모드에서 노드는 Root 노드를 오차 범위 내에서 변하지 않게 유지해, 캡슐이 부모로서 회전해도 Mesh의 Root가 따라 회전하지 않게 한다. GASP에서 MaxRotationError 값은 -1로 무한대를 의미한다. 방금 그 영상과 같은 조건에서 OffsetRootBone을 켜고 60° 회전하면, 캡슐은 이미 회전해 있지만 우리의 Root는 여전히 변하지 않는 것을 볼 수 있다.

Root 문제를 해결했으니, 이어서 턴 애니메이션의 로직을 볼 수 있다. shouldTurnInPlace가 트리거된 후, 상태는 Transition to Idle 상태로 점프 진입하고, Chooser를 트리거해 FutureFacingDelta에 따라 적합한 애니메이션을 선택한다. 여기서 주의할 점은 UseMM=False라는 것이다. 그래서 바로 정상 재생되고 이 애니메이션에 MotionMatching으로 적합한 위치를 선택하지 않는다.

먼저 이 애니메이션을 직접 재생하면 아래 그림에서 우리 캐릭터가 대략 80여 도 도는 모습을 볼 수 있고, 애니메이션의 90°에 부합하지 않는다. 이는 Idle에서 Turn 애니메이션으로 전환할 때 융합 시간 0.4초가 있어, 융합 단계에서 Turn 애니메이션의 RootRotation이 전부 적용되지 않아 약간의 편차가 생기기 때문이다. 끄면 정상적인 90°가 된다. (ALS에서도 RotationAmount 커브에 똑같이 이 문제가 있다)

이때 문제가 온다. 우리에게는 90° 애니메이션 하나뿐인데, 융합 간섭이 있는 상황에서도 딱 60° 위치나 다른 여러 각도로 돌게 하려면 어떻게 해야 할까? 또 위에서 OffsetRoot의 Accumulate 모드도 있다고 했는데, 이동할 때는 어떻게 올바르게 회전할 수 있을까?
이 문제들을 GASP는 모두 Steering 노드로 해결했다. 이 노드의 역할은 애니메이션의 회전을 스케일 조정해, Root가 TargetOrientation 방향으로 회전하게 하는 것이다.
애니메이션을 스케일해야 하므로 미래 일정 시간의 애니메이션 변위를 알아야 한다. 그래서 AnimAsset이 있고 AnimatedTargetTime을 전달해 뒤로 얼마나 예측할지를 나타낸다. 예측 시간이 충분히 길지 않으면 애니메이션 스케일이 불균일해져 애니메이션 본래의 회전 리듬을 파괴한다.
Steering이 유효한 동안에는 플레이어가 여전히 조작할 수 있어서, 1초 전에는 30° 돌기를 원하다가 다음 1초에는 135°일 수 있다. 그래서 Steer는 동적으로 따라간다.

Steering 내부의 방법은 ALS의 원리와 매우 비슷하다. 예측된 실제 필요 회전 각도와 애니메이션의 회전 각도로 스케일하는데, 예를 들어 애니메이션은 90°인데 60°를 돌아야 하면 매번의 회전량을 66.7%만 적용한다. 하지만 동시에 DistanceMatch의 사상도 있다. 다만 시간 수정에 쓰이지 않고 Root 수정에 쓰인다.
예를 들어 60°를 돌아야 하고, 3초에 90°를 도는 애니메이션이 하나 있다. 앞의 2초에는 20°를 돌고 마지막 1초에 70°를 도는, 처음에는 느리고 나중에 빠른 형태인데, 내 예측 시간은 2초만 열려 있다.
그래서 시작할 때, 애니메이션에 20°가 있고 실제로는 60°가 있다고 예측한다. 이론상 3배 스케일해야 하지만, Max/Min ScaleRate 설정이 있어 1.5배만 스케일된다. 앞의 2초에 30°를 돌았지만, 뒤의 1초는 스케일 제한 때문에 최대 0.5배까지만 스케일돼 35°를 돌았다. 최종적으로 실제 회전은 기대한 60°가 아닌 65°가 된다. DistanceMatch의 AdvanceTime과 매우 비슷하다.
예측 시간 부족 때문일 수도 있고, 회전 과정에서 Target이 변화해 Root가 스케일 범위 내에서 만족할 수 없기 때문일 수도 있다. 그래서 Root 회전 스케일 외에도 Steering에는 보정 보상 부분이 있다. RunFLoop처럼 루트 회전이 없는 애니메이션도 정상적으로 회전할 수 있는 것도 보정 보상이 작용하기 때문이다.
그 구현은 스프링 스무싱 보상을 통하는 것이고, ProceduralTargetTime이 스무싱의 오차 반감기를 결정한다. 작을수록 스무싱이 더 빠르다.
GASP에서 TargetOrientation은 현재의 기대 방향을 쓰지 않고 궤적에 따라 예측한다. 이렇게 하면 회전 노드가 미래의 방향을 향해 회전할 수 있고, 항상 현재 캐릭터의 회전 방향을 향해 회전하지 않아 뒤처짐이 너무 길어질 수 있는 문제를 피한다.

이론적으로는 이 노드 하나로 제자리 턴과 애니메이션 방향 문제를 해결할 수 있어야 하지만, GASP는 현재 두 개의 Steering 노드를 만들었다. 제자리 턴 시 두 번째 것도 유효해진다. 이들의 주요 차이는 RootMotionAngleThreshold가 -1에서 1로, Disable Steering Below Speed가 10에서 -1로 바뀐 것이다
두 번째 Steering의 Disable Steering Below Speed는 제자리에서 움직이지 않을 때 Steering이 먹히지 않는 것을 피할 수 있다. RootMotionAngleThreshold는 애니메이션의 RootMotion이 이 각도보다 작으면 RootScale 과정을 실행하지 않는다. 하지만 두 번째 Steering의 폭에서는 별 쓸모가 없어 보인다?

5.4 FootLock
위의 어떤 방법이든, 회전량을 조정한 후에는 발 미끄러짐 문제가 나타난다. 이에 대한 해결 방법이 FootLock이다. 발이 땅에 놓였을 때 그것을 잠가, 캐릭터 전체 회전과 맞지 않아 미끄러지지 않게 한다.
GASP에서는 FootPlacement라고 부르는데, 방법도 매우 관례적이다. 애니메이션에 커브 마커를 달아 어느 시각에 발이 내려앉는지를 기록하고, 이후 해당 시각에 발을 잠그면 된다.

그 구체적인 구현은 ControlRig에 있다. 그 안에서 GASP는 움직임을 수정하는 작은 디테일도 많이 만들었는데, 여기까지 오니 분량이 꽤 커졌으니, ControlRig의 것들은 뒤의 글에서 다시 탐구하겠다.
6. 맺음말
위에서 분석한 것은 GASP의 회전 주요 구상일 뿐이며, 동시에 ALS, Lyra 등 GASP 밖의 다른 구현 방안도 비교했다.
하지만 그중 디테일 처리는 아주 아주 많다. 이전 글 Mover에서 언급한 Rotation이 따라가지 못하는 것을 피하는 것과 등 뒤 턴 디테일, 그리고 연속 턴 상황에서 pivot 애니메이션 선택을 피하는 것 같은 것들, 그리고 언급하지 못한 수많은 것들이 있다
【UE5】차세대 캐릭터 이동 솔루션 Mover 가벼운 분석
[번역] UE5 차세대 캐릭터 이동 솔루션 Mover 분석
역자의 말: 프로토타입 준비 한답시고 요즘은 통 ChatGPT 와 놀고 있느라 정신이 있는건지 없는건지 모르겠네요. 여전히 지금 만들 게임에서는 ALS 면 충분해 보이긴 합니다만 GASP 업데이트 소식도
techartnomad.tistory.com
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] 《원신》에서 《Nodusfall》까지, 미호요는 왜 협동 전투 게임을 시작했을까. (0) | 2026.08.26 |
|---|---|
| UF2025(상하이) 《명조》 오픈월드 2차원 게임의 레이 트레이싱 실전 Lumen+DXR (0) | 2026.08.26 |
| [번역] UE5 차세대 캐릭터 이동 솔루션 Mover 분석 (0) | 2026.08.24 |
| [Siggraph 2026] 《콜 오브 듀티: 모던 워페어 4》의 가변 레이트 레이 트레이싱(Variable Rate Ray Tracing) (0) | 2026.08.18 |
| [SIGGRAPH 2026] SLIM: 로블록스(Roblox)의 사용자 생성 3D 월드 대규모 확장 기법 (0) | 2026.08.17 |