2023. 8. 21. 15:43ㆍGame Development/Unreal Engine
1. 글을 쓰게 된 계기
애니메이션 블루프린트로 캐릭터 애니메이션을 적용하던 중, 여러 자료를 찾아보며 공부 하다보니, 언리얼 엔진은 여러 스레드로 구성되어 돌아간다는 사실을 알게 되었고, 애니메이션 또한 그렇다는 사실을 알게 되었습니다. 하지만 생각보다 애니메이션 스레드 관련 자료가 많이 없었기에, 제가 작성하여 이를 공유하고자 글을 작성하게 되었습니다.
언리얼 엔진을 공부하는 입장이기에 모두 정확한 사실은 아닐 수 있어, 참고하였던 문서 및 영상 링크를 남겨두겠습니다.
참고 자료
- 🔗[Youtube] Understanding Render Thread and Animation Thread in Unreal Engine
- 🔗[UE 5.2 Documentation] 애니메이션 최적화
- 🔗[UE 5.2 Documentation] 스레드 세이프하게 변수 가져오기
2. [사전 지식] 스레드 동기화 문제
애니메이션 얘기를 하기 위해선, 먼저 언리얼 엔진이 스레드 동기화 문제를 어떻게 해결했는지를 알아야 합니다. 우리는 성능 향상을 위해 멀티스레드를 사용하지만, 스레드들 간에 서로 통신하며 데이터를 주고받아야 할 경우에는 조심할 필요가 생깁니다. 언리얼 엔진에서 실행되는 게임 스레드(Game Thread)와 렌더 스레드(Render Thread)가 바로 이러한 관계에 놓여 있습니다.
- 게임 스레드(Game Thread)
입력, 트랜스폼 변경, 기타 등등 게임과 관련된 모든 로직을 처리합니다. - 렌더 스레드(Render Thread)
게임 내에서 렌더링 해야 할 데이터 정보들을 읽어서 처리합니다.
렌더 스레드는 게임 스레드 내에 존재하는 컨트롤러, 메시, 트랜스폼 등의 데이터를 읽어와야 거기에 맞춰 렌더링을 할 수 있습니다. 하지만 렌더 스레드가 렌더링을 하는 동안 게임 스레드 내에서 렌더 스레드가 읽고 있는 데이터를 변경해 버린다면, 렌더 스레드는 잘못된 내용을 처리하고 있는 셈이 됩니다.
이것은 동기화가 제대로 일어나지 않아서 발생하는 문제이며, 해결하는 가장 간단한 방법은 한 쪽 스레드에서는 Lock을 걸고, 다른 스레드에서는 해당 스레드의 Lock이 풀릴 때까지 대기하는 것입니다.
하지만 이 방법은 그다지 효율적인 방법이 아닙니다. 씬(Scene)의 내용이 복잡해질수록 렌더 스레드는 기다리는 데에 시간을 대부분 쏟아야 합니다.
3. 언리얼 엔진이 선택한 해결 방법
언리얼 엔진에서는 위 문제에 대해, 게임 스레드의 모든 데이터를 렌더 스레드로 복사하여 이 문제를 해결합니다.
- 게임 스레드가 한 프레임 끝에서 모든 액터를 가져옵니다.
- 그 후, 렌더 스레드 하나가 해당 한 프레임의 모든 게임 스레드 데이터를 복사합니다.
- 복사가 끝난 후, 게임 스레드와 렌더 스레드가 같이 실행됩니다.
- 게임 스레드는 다시 로직 처리를 실행합니다.
- 렌더 스레드는 이전 게임 스레드의 데이터를 렌더링합니다.
이렇게 하면, 게임 스레드가 작업하는 동안 렌더 스레드도 기다리지 않고 작업을 계속 할 수 있습니다. 이러한 이유로 인해, 렌더 스레드는 항상 게임 스레드보다 1~2 프레임 뒤처지게 됩니다. 하지만 두 스레드가 동시에 작업이 완료되진 않습니다. 스레드는 독립적이며, 각자 처리 속도가 다르기 때문이죠.
1) 렌더 스레드가 게임 스레드보다 빠른 경우
게임 로직이 복잡하나, 렌더링 할 요소는 얼마 없는 경우에 해당합니다. 게임 스레드가 한 프레임 완료되면, 렌더 스레드가 해당 프레임을 렌더링하고, 남은 시간은 대기하면 됩니다.
2) 게임 스레드가 렌더 스레드보다 빠른 경우
게임 로직에 비해, 렌더링 할 요소들(나뭇잎, 지형 등등)이 많아 렌더링에 많은 시간이 소요될 경우에 발생합니다. 이 경우에는 게임 스레드가 렌더 스레드를 기다리지 않습니다. 입력 처리에 좋기 때문입니다. 반응 처리는 이 경우가 더 좋네요.
게임 스레드가 렌더 스레드를 기다리지 않기 때문에, 두 스레드 간 프레임 차이가 너무 나지 않도록 처리해주는 것이 중요합니다.
위 그림처럼 렌더 스레드가 2 프레임을 렌더링하고 있을 때, 게임 스레드는 이미 5 프레임이므로, 렌더 스레드는 3, 4 프레임을 건너 뛰고 5 프레임을 렌더링 하여 지연을 방지하고 있습니다.
4. 애니메이션 스레드
게임 스레드와 독립적으로 실행되는 렌더 스레드와 달리, 애니메이션 스레드는 게임 스레드와 함께 고정적으로 실행됩니다. 이것은 (1)게임 스레드가 씬의 모든 스켈레탈 메시를 가져오고, (2)각각에 대한 애니메이션 스레드를 시작하며, (3)게임 스레드는 모든 애니메이션 스레드가 작업을 완료할 때까지 기다렸다가 다음 프레임으로 진행됩니다.
보통 애니메이션 스레드 실행은 게임 스레드 작업보다 훨씬 전에 완료되므로, 게임 스레드에게 치중된 일을 애니메이션 스레드에게 분배해줌으로써 성능을 향상시킬 수 있습니다. 단일 애니메이션 Tick은 세 단계로 나뉘어 진행됩니다.
- 게임 스레드에서 스켈레탈 메시가 생성되고, 생성 작업이 끝나면 별도의 애니메이션 스레드로 분리됩니다.
- 분리된 애니메이션 스레드가 애니메이션과 관련된 작업을 처리합니다.
- 애니메이션 작업이 다 끝났다면, 다시 게임 스레드로 돌아와서 해당 스켈레탈 메시에 대한 Tick을 처리합니다.
이러한 단계의 작업들은 애니메이션 블루프린트에서 두 부분으로 나뉘어져서 처리됩니다.
1) 이벤트 그래프(Event Graph)
이벤트 그래프에서는 매 프레임마다 호출되는 Update Animation Event와 함께 제공됩니다. Update Animation Event 노드 아래에 배치되는 모든 로직들은 전부 게임 스레드에서 실행됩니다. 해당 작업은 첫 번째 단계에서 수행되는 작업이며, 여기에서 발생하는 모든 일은 게임 스레드가 처리하게 됩니다. 즉, 별도의 애니메이션 스레드에서 실행되지 않는다는 것을 의미합니다
2) 애님 그래프(Anim Graph)
애님 그래프는 애니메이션 스레드에서 두 번째 단계에 해당하며, 애니메이션 스레드에서 수행하는 유일한 작업입니다.
애니메이션 스레드의 포즈(Pose)를 평가하기 위해, 애님 그래프가 실행되기 전에 호출되는 BlueprintThreadSafeUpdateAnimation 함수가 존재하며, 해당 함수 내에서 돌아가는 작업들이 애니메이션 스레드들입니다.
허나 게임 스레드에서 애니메이션 스레드로 직접 값을 읽어오는 것은 안전하지 않기 때문에, 렌더 스레드에서 적용했던 것처럼 게임 스레드에서 필요한 데이터를 애니메이션 스레드로 복사하는 과정이 진행됩니다. 그 후에 해당 함수 내에서 데이터를 사용할 수 있게 됩니다.
5. 실제 사용 예시
언리얼 엔진에서 기본으로 제공되는 3인칭 템플릿 캐릭터의 애니메이션 블루프린트 클래스를 보면, 다음과 같이 UpdateAnimationEvent에서 캐릭터에 대한 데이터를 모으고 있습니다.
위에서 언급했듯, UpdateAnimationEvent 노드 아래에서 실행되는 모든 로직들은 게임 스레드에서 실행되므로, 여기에 너무 무거운 연산을 넣게 되면 병목 현상이 생겨 성능이 감소하게 됩니다. 게임 스레드는 할 일이 많은데, 애니메이션 스레드는 할 일이 많지 않기 때문입니다. 따라서, 이러한 게임 스레드의 작업량을 애니메이션 스레드에게 맡겨 줄 필요가 있습니다.
아래 사진은 애니메이션 스레드에게 위에서 했던 일을 맡긴 모습입니다.
즉, 애니메이션 포즈가 평가되기 전에 필요한 데이터들을 애니메이션 스레드에서 계산하여 변수에 저장한 후, 애니메이션이 작동할 때 해당 데이터를 사용하여 작동하는 방식입니다. 이러한 방식은 게임 스레드 부하를 크게 줄여줄 수 있지만, 데이터를 저장해서 써야 하므로 애니메이션 최종 포즈 평가에 필요 없는 변수가 많이 생길 수 있는 단점이 존재합니다.
6. C++에서의 사용 예시
게임 스레드의 데이터를 애니메이션 스레드로 복사하는 과정을 위해, 그 둘 사이에 🔗FAnimInstanceProxy라는 구조체가 있습니다. 여기서부터는 글로 설명하는 것보다, 제가 참고했던 영상을 보며 확인하는 것이 더 빠를 것 같습니다. 해당 타임 라인 좌표를 찍은 🔗영상 링크입니다.
7. 마무리
저는 공부하다가 궁금한 게 있으면 파들어 가는 성격이기에, 공부하다 보니 프레임워크 공부까지 되었네요... 🔗이 영상을 보고 나니, 한 번 제 프로젝트에도 적용해 봐야겠다고 결심하게 되었습니다. 제가 정리한 내용들이 다 정확하진 않을 수 있기에 틀린 부분이 있다면, 가르쳐 주시면 감사하겠습니다 :)
'Game Development > Unreal Engine' 카테고리의 다른 글
[UE5] Unreal Engine 디버깅 하는 법 with Visual Studio (0) | 2023.02.09 |
---|