CPU, GPU 병목 현상 해결
이전 절들에서 우리는 성능 프로파일링 도구 사용법과 메모리 및 렌더링 최적화 기법에 대해 자세히 알아보았습니다. 이제는 게임 성능 저하의 가장 직접적인 원인이 되는 CPU 및 GPU 병목 현상을 식별하고 해결하는 구체적인 전략에 집중할 차례입니다. 게임의 프레임 속도(FPS)는 CPU와 GPU 중 더 느린 쪽의 영향을 받습니다. 즉, 게임이 CPU 바운드(CPU Bound)라면 CPU가 더 느리고, GPU 바운드(GPU Bound)라면 GPU가 더 느린 것입니다.
이번 절에서는 각 병목 현상의 특성을 이해하고, 이를 해결하기 위한 실질적인 접근 방식과 언리얼 엔진의 특정 기능을 활용한 최적화 방안을 심층적으로 다루겠습니다.
병목 현상 진단: CPU vs GPU
최적화의 첫걸음은 병목 현상이 어디서 발생하는지 정확히 아는 것입니다. 언리얼 엔진에서 가장 유용한 도구는 stat unit
콘솔 명령어입니다.
stat unit
결과 해석- Game: CPU의 게임 스레드(Game Thread) 시간. 액터 업데이트, 물리, AI, 애니메이션 로직 등 게임 플레이 관련 CPU 작업.
Game
값이 높으면 CPU Bound (Game Thread). - Draw: CPU의 렌더 스레드(Render Thread) 시간. 렌더링 명령 준비 및 RHI로 전송하는 CPU 작업.
Draw
값이 높으면 CPU Bound (Render Thread). - GPU: 실제 GPU 렌더링 시간. 픽셀 처리, 정점 처리, 셰이더 실행 등 GPU 작업.
GPU
값이 높으면 GPU Bound. - RHI: RHI 스레드 시간. 그래픽 API 호출 관련 CPU 오버헤드.
- Game: CPU의 게임 스레드(Game Thread) 시간. 액터 업데이트, 물리, AI, 애니메이션 로직 등 게임 플레이 관련 CPU 작업.
이 값을 통해 현재 게임이 CPU 바운드인지, GPU 바운드인지, 그리고 CPU 바운드라면 Game Thread 문제인지 Render Thread 문제인지를 즉시 파악할 수 있습니다.
CPU 병목 현상 해결
CPU 병목 현상은 게임 로직 또는 렌더링 명령 준비 과정에서 CPU가 과도하게 사용될 때 발생합니다.
Game Thread 최적화
Game Thread는 게임플레이 로직의 대부분을 처리합니다. 이곳에서 병목이 발생하면 게임 업데이트가 느려져 프레임이 떨어집니다.
해결 전략
- 액터 및 컴포넌트 업데이트 최적화
- Tick 함수 최소화:
AActor::Tick()
및UActorComponent::Tick()
함수는 매 프레임 실행되므로, 이곳에 무거운 로직을 넣지 않도록 주의합니다.bCanEverTick
을false
로 설정하여 불필요한 틱을 비활성화합니다. - 틱 그룹(Tick Groups) 활용: 특정 액터의 틱 업데이트 순서를 제어하고, 중요한 액터가 먼저 업데이트되도록 하거나, 덜 중요한 액터는 더 늦게 업데이트되도록 조정합니다.
- 틱 간격 조절:
SetActorTickInterval()
을 사용하여 특정 액터가 매 프레임이 아닌, 예를 들어 0.1초마다 한 번씩만 틱되도록 설정합니다. 멀리 있는 NPC나 환경 오브젝트에 유용합니다. - 컴포넌트 단위 최적화: 액터 전체가 아닌 필요한 컴포넌트만 틱하도록 설정하고, 불필요한 컴포넌트는 비활성화합니다.
- Tick 함수 최소화:
- 물리 시뮬레이션 최적화
- 물리 바디 수 줄이기: 복잡한 물리 시뮬레이션은 CPU 자원을 많이 소모합니다. 불필요한 물리 오브젝트는
Simulate Physics
를 끄거나, Collision Preset을No Collision
으로 설정합니다. - 콜리전 복잡도 단순화: 복잡한 메시의 경우
Complex Collision As Simple
을 사용하거나,UCapsuleComponent
,UBoxComponent
등 단순한 프리미티브 콜리전을 사용하여 충돌 검사 비용을 줄입니다. - PhyX Scene 업데이트 빈도 조절:
Project Settings
->Physics
에서 물리 업데이트 빈도를 조절할 수 있습니다.
- 물리 바디 수 줄이기: 복잡한 물리 시뮬레이션은 CPU 자원을 많이 소모합니다. 불필요한 물리 오브젝트는
- AI 및 Pathfinding 최적화
- 거리 기반 AI 비활성화: 플레이어로부터 멀리 떨어진 AI는 업데이트 빈도를 줄이거나 완전히 비활성화합니다. Behavior Tree의
Services
나Decorators
에서 조건부로 AI 로직을 실행하도록 합니다. - Pathfinding 복잡도: 복잡한 내비게이션 메시(Navigation Mesh) 생성 비용을 줄이고,
RecastNavMesh
설정에서 셀 크기 등을 조절합니다. - AI 병렬 처리: 일부 AI 시스템은 멀티스레딩을 활용하여 CPU 코어를 분산하여 사용합니다.
- 거리 기반 AI 비활성화: 플레이어로부터 멀리 떨어진 AI는 업데이트 빈도를 줄이거나 완전히 비활성화합니다. Behavior Tree의
- 애니메이션 최적화
- LOD (Level Of Detail): 스켈레탈 메시의 LOD를 설정하여 거리에 따라 더 적은 본(Bone)과 복잡도로 애니메이션을 계산합니다.
- 애니메이션 블루프린트 복잡도: 애니메이션 블루프린트의
Event Graph
와Anim Graph
에서 복잡한 로직을 최소화합니다.Evaluate Graph
노드의 비용을 확인하고 줄입니다. - 애니메이션 컬링: 시야 밖에 있는 캐릭터나 작은 캐릭터의 애니메이션 업데이트를 중단하거나 최소화합니다.
- Pose Asset 및 Anim Montages: 효율적인 애니메이션 블렌딩을 위해 사용합니다.
- 오브젝트 풀링: 총알, 파티클 효과, 임시 액터 등 자주 생성되고 파괴되는 오브젝트는 미리 생성해두고 재활용하는 오브젝트 풀링을 구현하여
SpawnActor
/DestroyActor
호출로 인한 CPU 오버헤드를 줄입니다. - 블루프린트 → C++ 마이그레이션: 성능에 민감한 로직은 블루프린트보다 C++로 구현하는 것이 일반적으로 더 빠릅니다.
Render Thread 최적화
Render Thread는 게임 월드를 렌더링하기 위한 명령을 준비하고 그래픽 API(RHI)로 전송하는 역할을 합니다. 이곳에서 병목이 발생하면 드로우 콜(Draw Calls) 수가 많거나 렌더링 상태 변경이 잦을 가능성이 큽니다.
해결 전략
- 드로우 콜 감소 (핵심!)
- 인스턴싱(Instancing): 동일한 메시를 여러 번 배치하는 경우
UInstancedStaticMeshComponent
또는UFoliageType
을 사용하여 단일 드로우 콜로 수천 개의 인스턴스를 렌더링합니다. - 액터 머지(Actor Merging): 에디터의
Merge Actors
기능을 사용하여 인접한 여러 스태틱 메시를 하나의 메시로 병합합니다. 동일한 머티리얼을 사용하는 오브젝트들을 합치면 효과가 극대화됩니다. - HLOD (Hierarchical Level of Detail): 대규모 오픈 월드에서 멀리 있는 여러 액터들을 하나의 최적화된 메시와 머티리얼로 자동 병합하여 드로우 콜을 줄입니다.
- 텍스처 아틀라스(Texture Atlases): 여러 작은 텍스처를 하나의 큰 텍스처에 묶어 사용하여 텍스처 바인딩 및 셰이더 변경 비용을 줄입니다.
- 인스턴싱(Instancing): 동일한 메시를 여러 번 배치하는 경우
- 컬링(Culling) 최적화
- 오클루전 컬링 (Occlusion Culling): 시야에 가려진 오브젝트를 렌더링하지 않도록 합니다. 언리얼 엔진은 기본적으로 하드웨어 오클루전 컬링을 사용합니다.
- Precomputed Visibility Volume: 정적인 씬에서 미리 계산된 가시성 데이터를 사용하여 런타임 컬링 효율을 높입니다.
- Foliage 및 Instanced Static Mesh 컬링: 인스턴싱된 오브젝트의 컬링 설정을 조정하여 불필요한 렌더링을 줄입니다.
- 광원 및 그림자 최적화
- 동적 광원 수 제한: 동적 광원은 드로우 콜과 셰이더 복잡도를 증가시킵니다. 가능한 한
Static
또는Stationary
광원을 사용하고,Movable
광원 수는 최소화합니다. - 그림자 최적화:
Cast Shadows
비활성화, 그림자 맵 해상도 감소, 캐스케이드 수 조절 등 렌더링 최적화 절에서 다룬 기법들을 적용합니다.
- 동적 광원 수 제한: 동적 광원은 드로우 콜과 셰이더 복잡도를 증가시킵니다. 가능한 한
- 복잡한 머티리얼 제거/단순화: 렌더 스레드는 복잡한 셰이더 컴파일 및 상태 변경으로 인해 부담을 받을 수 있습니다.
Shader Complexity
모드를 사용하여 복잡한 머티리얼을 식별하고 단순화합니다.
GPU 병목 현상 해결
GPU 병목 현상은 GPU가 픽셀 처리, 정점 처리, 셰이더 계산 등 실제 렌더링 작업을 수행하는 데 시간이 오래 걸릴 때 발생합니다. profilegpu
명령어가 이 문제를 진단하는 데 가장 유용합니다.
해결 전략
- 픽셀 복잡도 감소 (핵심!)
- 오버드로우(Overdraw) 감소: 투명 또는 마스크드 머티리얼 사용을 최소화하고, 파티클 시스템의 오버드로우를 최적화합니다.
Shader Complexity
모드를 사용하여 과도한 오버드로우 영역을 찾습니다. - 머티리얼 셰이더 복잡도: 머티리얼 에디터의
Stats
탭을 확인하여 셰이더 명령 수를 줄입니다. 불필요한 연산, 텍스처 샘플링을 제거합니다. - 렌더링 기능 비활성화: 프로젝트 설정(
Project Settings
->Rendering
)에서 필요 없는 렌더링 기능(예:Motion Blur
,Bloom
,Ambient Occlusion
등)을 비활성화하거나 품질을 낮춥니다.
- 오버드로우(Overdraw) 감소: 투명 또는 마스크드 머티리얼 사용을 최소화하고, 파티클 시스템의 오버드로우를 최적화합니다.
- 폴리곤 수 감소
- LOD 적극 활용: 렌더링 최적화 절에서 다룬 스태틱 및 스켈레탈 메시의 LOD를 사용하여 원거리 오브젝트의 폴리곤 수를 줄입니다.
- 최적화된 모델링: 에셋 제작 단계에서 폴리곤 수를 최소화하고 노멀 맵으로 디테일을 표현합니다.
- 해상도 및 스케일링 조절
- 화면 해상도: 게임의 기본 해상도를 낮추거나,
r.ScreenPercentage
콘솔 변수를 사용하여 렌더링 해상도를 줄이면 GPU 부하를 크게 낮출 수 있습니다. (시각적 품질 저하가 발생할 수 있음) - 업스케일링 기술: FSR (FidelityFX Super Resolution), DLSS (Deep Learning Super Sampling)와 같은 업스케일링 기술을 적용하여 낮은 해상도로 렌더링하고 고해상도로 출력하여 GPU 부담을 줄이면서도 시각적 품질을 유지합니다.
- 화면 해상도: 게임의 기본 해상도를 낮추거나,
- 그림자 맵 품질 및 종류 조절:
r.Shadow.MaxResolution
,r.Shadow.TexelsPerPixel
, 캐스케이드 수 등을 조절하여 그림자 렌더링 비용을 낮춥니다. 실시간 레이 트레이싱 그림자 대신 라이트맵이나 캡슐 그림자를 고려합니다. - 후처리 효과 최적화: Post Process Volume에서 각 후처리 효과의 품질 설정을 조정합니다. 복잡한 커스텀 후처리 셰이더를 사용할 때는 성능을 면밀히 검토해야 합니다.
- 파티클 시스템 최적화: 과도한 수의 파티클, 복잡한 파티클 머티리얼, 높은 오버드로우를 유발하는 파티클은 피합니다. LOD와 컬링을 활용합니다.
기타 고려 사항 및 고급 기법
- 멀티스레딩 활용: 언리얼 엔진은 기본적으로 멀티스레딩 아키텍처를 사용하지만, 특정 게임 로직을 수동으로
FAsyncTask
나Async
기능을 사용하여 비동기적으로 실행하고 Game Thread의 부하를 줄일 수 있습니다. (예: 데이터 로딩, 복잡한 계산) - 데이터 지향 설계 (Data-Oriented Design, DOD): 캐시 효율을 높이기 위해 데이터를 메모리에 연속적으로 배치하고, 유사한 데이터를 함께 처리하여 CPU의 캐시 미스(Cache Miss)를 줄이는 설계 방식입니다. 언리얼 엔진의 ECS (Entity Component System) 유사 기능인
Mass Framework
등이 이에 해당합니다. - 프로젝트 설정 조정:
Project Settings
->Engine
->Rendering
또는Physics
등에서 전반적인 품질 설정을 조정할 수 있습니다. - 콘텐츠 드라이븐 최적화: 개발 단계부터 에셋 제작 가이드라인을 설정하여 최적화된 모델, 텍스처, 머티리얼을 제작하도록 유도합니다.
CPU와 GPU 병목 현상을 해결하는 것은 복잡하지만 게임의 성능을 결정짓는 가장 중요한 요소입니다. stat unit
을 통한 초기 진단 후, Game Thread, Render Thread, GPU 각각의 특성에 맞는 최적화 전략을 적용해야 합니다. 틱 최적화, 물리/AI/애니메이션 최적화, 드로우 콜 감소, 인스턴싱, LOD, 오버드로우 감소, 그림자 및 후처리 효과 관리 등 다양한 기법들을 꾸준히 적용하고, Unreal Insights
와 profilegpu
와 같은 전문 프로파일링 도구를 사용하여 개선 효과를 측정하며 반복적으로 최적화를 수행하는 것이 안정적이고 고성능의 게임을 만드는 길입니다.