환경 요소의 성능 분석
프레임 시간과 CPU·GPU·메모리 지표를 프로파일링하고 환경 요소별 비용을 분리해 실제 성능 병목을 찾습니다.
레벨에 지형과 조명, 날씨, 물리, 상호작용까지 올렸다면, 이제 보기 좋은 장면을 계속 유지할 수 있는 성능으로 바꿀 차례입니다. 레벨이 복잡해질수록 CPU와 GPU, 메모리의 부담은 빠르게 늘어납니다. 그래서 성능 최적화(performance optimization)는 실행 속도를 높이는 일에 그치지 않고, 여러 기기에서 안정된 프레임과 일관된 경험을 목표로 측정하고 고치는 과정입니다. 그리고 그 출발점은 짐작이 아니라 측정입니다.
이 글에서는 성능 최적화의 목표를 정리하고, CPU와 GPU가 각각 무엇을 하며 언제 병목이 되는지 살펴봅니다. 그다음 Stat 명령과 ProfileGPU, 뷰포트 시각화 모드 같은 분석 도구를 익히고, 메시, 재질, 조명, 파티클, 물리, 후처리마다 무엇을 보고 어떻게 줄이는지 정리합니다. 마지막으로 측정한 값으로 먼저 조사할 곳을 고르는 순서를 계산 예와 함께 따라갑니다.
성능 최적화의 목표
성능 최적화는 개발의 마지막에 한 번 하는 일이 아니라, 개발 내내 이어지는 작업입니다. 가장 먼저 지켜야 할 것은 프레임 속도입니다. 플레이어가 쾌적하게 플레이하려면 안정된 프레임이 꼭 필요하므로, PC는 60 FPS, 모바일은 30 FPS처럼 목표를 정하고 그것을 지킵니다.
최적화는 다른 이점도 줍니다. 저사양 기기에서도 게임이 원활하게 돌아가 더 많은 플레이어가 즐길 수 있고, 모바일에서는 배터리 소모가 줄어 플레이 시간이 늘어납니다. 잘 최적화된 프로젝트는 빌드 시간이 줄고 에디터도 가벼워져 개발 자체가 빨라지며, 성능이 떨어져 생기는 물리 버그나 로딩 지연 같은 문제도 미리 막을 수 있습니다.
CPU와 GPU는 각각 무엇을 하나
분석을 시작하기 전에, 프레임을 만드는 두 처리 장치가 무엇을 맡는지 알아야 합니다. 한 프레임은 두 장치가 각자의 일을 모두 끝내야 완성되므로, 프레임 시간은 둘 가운데 더 느린 쪽이 정합니다. 그래서 느린 쪽이 어디인지 모른 채 다른 쪽을 최적화하면, 그 작업 시간이 줄어도 프레임 속도는 거의 그대로입니다. 아래에서는 두 장치가 하는 일과, 각 장치를 느리게 만드는 흔한 원인을 나눠 봅니다.
CPU
CPU는 게임 로직과 물리 시뮬레이션, AI 계산, 애니메이션 섞기, 그리고 무엇을 그릴지 정리해 GPU에 넘기는 그리기 호출(draw call)의 준비처럼, 게임의 진행과 렌더링 명령의 제출에 필요한 일을 합니다. CPU의 작업이 GPU보다 느려 전체 프레임 속도를 제한하는 상태를 CPU 바운드(CPU bound)라고 부릅니다.
CPU 병목의 흔한 원인은 다음과 같습니다. Event Tick에서 너무 복잡한 계산을 하는 블루프린트나 C++ 로직, 레벨에 배치된 액터가 너무 많거나 복잡한 로직을 가진 액터가 많은 경우, 동시에 많은 물체를 시뮬레이션하는 물리, 복잡한 AI 로직이나 많은 AI 캐릭터입니다. 그리기 호출도 CPU의 비용인데, CPU가 GPU에 그릴 물체를 넘기는 횟수가 많을수록 CPU의 부담이 늘어납니다.
GPU
GPU는 메시를 그리고, 셰이더를 계산하고, 후처리와 그림자, 조명을 계산하는 그래픽 연산을 합니다. 다만 그래픽 작업에도 CPU가 준비하고 제출하는 비용이 따로 있다는 점을 기억해 둡니다. GPU의 작업이 CPU보다 느려 전체 프레임 속도를 제한하는 상태를 GPU 바운드(GPU bound)라고 부릅니다.
GPU 병목의 흔한 원인은 화면에 보이는 폴리곤이 너무 많은 경우, 파티클이나 유리, 식물처럼 투명하거나 반투명한 물체가 많이 겹쳐 같은 픽셀을 여러 번 그리는 오버드로(overdraw), 계산이 많은 복잡한 재질, 많은 실시간 그림자나 고해상도 그림자, 블룸과 앰비언트 오클루전, 화면 공간 반사 같은 비싼 후처리, 그리고 실시간 광원이 많거나 넓은 범위에 영향을 주는 광원입니다.
성능 분석 도구
Stat 명령
PIE를 실행한 뒤 콘솔에 Stat 명령을 입력하면 실시간 통계를 볼 수 있습니다. 콘솔을 여는 키는 키보드 배열과 프로젝트 설정에 따라 다르며, 보통 백틱(`)이나 물결표(~) 위치의 키입니다.
Stat FPS는 지금의 프레임 속도를 보여 주는 가장 기본적인 지표입니다. 가장 많이 쓰는 것은 Stat Unit으로, 프레임을 여러 축으로 나눠 밀리초 단위로 보여 줍니다. Game은 CPU에서 블루프린트와 AI, 물리 같은 게임 로직을 처리한 시간이고, Draw는 CPU가 렌더링 명령을 준비해 GPU에 넘기는 시간이며, GPU는 GPU가 그래픽을 그린 시간이고, Frame은 한 프레임 전체의 시간입니다.
Stat Unit을 읽을 때는 Game이 길면 CPU 바운드를, GPU가 길면 GPU 바운드를 의심합니다. Draw가 길면 렌더 스레드의 작업과 기다리는 시간을 더 조사합니다. 여기서 중요한 점은 Game, Draw, GPU는 서로 겹쳐서 동시에 실행될 수 있으므로 더하지 않는다는 것입니다. 또 Frame에는 동기화나 프레임 제한 때문에 기다리는 시간도 들어갑니다.
예산의 기준도 알아 둡니다. 60 FPS가 목표라면 한 프레임은 약 16.67 ms, 30 FPS라면 약 33.33 ms입니다. 같은 맵과 카메라, 해상도, 스케일러빌리티 조건에서 여러 프레임을 보고, 그 가운데 가장 긴 축부터 판단합니다.
다른 Stat 명령도 있습니다. Stat Engine은 엔진 전체의 카운터와 시간을 보여 주며, 개별 함수나 스레드의 병목은 그에 맞는 Stat 그룹과 언리얼 인사이트로 좁힙니다. Stat SceneRendering은 장면 렌더링 통계를 보는 출발점이고, GPU 단계별 시간은 Stat GPU나 ProfileGPU, GPU 트레이스로 확인합니다. Stat RHI는 렌더링 하드웨어 인터페이스(RHI)라는 하위 계층의 통계를 보여 줍니다. Stat D3D12RHI처럼 RHI마다 있는 통계는 엔진 버전과 RHI, 빌드에 따라 제공 여부와 항목이 다르므로, 쓰는 빌드의 통계 목록에서 확인합니다.
Game이나 Draw 축이 길 때는 언리얼 인사이트(Unreal Insights)를 씁니다. 게임 스레드(GameThread)와 렌더 스레드(RenderThread), RHI 스레드에서 실제로 어느 함수가 얼마나 오래 실행되었는지를 타임라인으로 보여 줍니다.
ProfileGPU
ProfileGPU는 GPU가 한 프레임을 그리는 동안 렌더링의 각 단계(pass)에 걸린 시간을 보여 줍니다. 게임 플레이 중에 콘솔에 ProfileGPU를 입력하면 되고, 결과는 버전에 따라 GPU Visualizer나 로그, 언리얼 인사이트 연동으로 확인합니다. 캡처 자체에도 부담이 있고 프레임마다 값이 조금씩 다르므로, 여러 번 측정해 비교합니다.
결과에서는 반투명과 파티클의 Translucency, 후처리의 PostProcess, 그림자 깊이의 ShadowDepths, 주요 메시를 그리는 BasePass 같은 단계 가운데 어디가 가장 오래 걸리는지 찾습니다. 그렇게 원인 후보를 찾은 뒤에는, 그 요소를 하나씩 바꿔 정말 그것 때문인지 확인합니다.
뷰포트 시각화 모드
레벨 에디터 뷰포트의 뷰 모드 메뉴에서도 병목을 눈으로 찾을 수 있습니다. Shader Complexity & Quads나 Quad Overdraw는 픽셀 처리와 겹침이 많은 영역을 보여 줍니다. 제공되는 모드와 색의 범례를 확인하되, 색을 GPU 밀리초로 읽지 않습니다. Shader Complexity는 재질의 셰이더 계산이 얼마나 복잡한지를 색으로 보여 주는데, 명령 수와 겹침으로 비용을 짐작한 것이라 텍스처를 읽는 비용 같은 실제 GPU 비용 전체를 나타내지는 않습니다.
Light Complexity는 표면에 영향을 주는 정적이지 않은 광원의 수를 색으로 보여 주어, 실시간 광원이 몰린 영역을 찾게 해 줍니다. Show 메뉴의 Collision과 충돌 뷰 모드는 물체의 충돌 형상을 보여 주어, 쓸데없이 복잡한 복잡 충돌을 찾을 수 있습니다. LOD Coloration은 각 메시가 지금 어느 LOD로 그려지는지 색으로 보여 주므로, LOD가 제대로 동작하는지, 멀리 있는 물체가 높은 LOD로 그려지고 있지는 않은지 확인할 수 있습니다.
도구를 고르는 순서
도구는 질문에서 출발해 고릅니다. 무엇이 궁금한지 정하고, 그것에 답하는 도구와 지표를 고른 뒤, 그 결과로 할 일을 정하면 병목의 원인을 짐작이 아니라 데이터로 좁힐 수 있습니다. 그래서 먼저 Stat Unit으로 Game, Draw, GPU를 나누고, 어느 축이 길었는지에 따라 다음 도구를 고릅니다. Game 축이라면 Tick과 AI, 물리, 블루프린트 호출을, Draw 축이라면 그리기 호출과 렌더링 명령의 제출을, GPU 축이라면 ShadowDepths, BasePass, PostProcess 같은 단계의 비용을 먼저 봅니다.
환경 요소별로 무엇을 볼까
메시
메시는 Stat SceneRendering이나 Stat RHI가 제공하는 그리기와 프리미티브 관련 카운터로 봅니다. 다만 BasePass의 시간 자체를 폴리곤 수로 읽지 말고, 나나이트를 쓰는지와 카운터가 무엇을 세는지를 함께 확인합니다. LOD Coloration 뷰 모드로 LOD가 제대로 바뀌는지, 너무 멀리 있는 물체가 높은 LOD로 그려지고 있지 않은지도 봅니다.
줄이는 방법은 쓸데없는 폴리곤을 줄이고, 알맞은 LOD를 설정하며, 서로 붙어 있는 작은 메시들을 액터 병합(Merge Actors) 기능으로 하나로 합쳐 그리기 호출을 줄이는 것입니다.
재질과 셰이더
재질은 Shader Complexity 뷰 모드로 복잡한 셰이더를 쓰는 물체를 찾고, ProfileGPU로 그 재질이 쓰이는 BasePass 같은 단계를 비교합니다. 단계 전체의 시간만으로 재질 하나의 비용을 확정할 수는 없습니다. 줄이는 방법은 픽셀 셰이더의 복잡한 계산과 텍스처 샘플러 수를 줄이고, 필요 없는 노드를 지우며, 오버드로를 만드는 반투명(Translucent) 재질을 되도록 적게 쓰는 것입니다.
조명
조명은 Light Complexity 뷰 모드로 실시간 광원이 몰린 영역을 찾고, ProfileGPU로 지금 렌더링 경로에서 조명과 그림자에 해당하는 단계의 비용을 봅니다. 가상 섀도 맵처럼 방식에 따라 단계의 이름과 구성이 달라집니다. 줄이는 방법은, 라이트맵을 쓰는 경로라면 Static과 Stationary를 검토하고, 실시간 조명이 필요하다면 Movable의 비용을 측정하는 것입니다. 루멘 글로벌 일루미네이션을 쓴다면 미리 계산한 정적 조명과의 차이부터 확인합니다. 광원의 Attenuation Radius는 필요한 만큼만 작게 정하고, 그림자 품질 설정도 조정합니다.
파티클과 특수 효과
파티클은 Shader Complexity & Quads나 Quad Overdraw로 반투명 파티클의 오버드로를 확인하고, ProfileGPU로 Translucency 같은 관련 단계를 보며 반투명 물체를 그리는 비용을 분석합니다. 줄이는 방법은 파티클의 생성 속도와 최대 수를 제한하고, 마스크드 재질을 검토해 오버드로를 줄이며, 나이아가라의 이펙트 타입과 스케일러빌리티로 거리와 인스턴스 수에 따른 컬링을 정하는 것입니다. 플랫폼과 기능의 제약을 확인한 뒤 GPU 시뮬레이션과 비교해 볼 수도 있는데, GPU 시뮬레이션이어도 시스템과 이미터의 CPU 비용은 남습니다.
물리 시뮬레이션
물리는 지금 버전이 제공하는 물리 통계를 Stat Physics로 보고, 자세한 원인은 언리얼 인사이트와 카오스 비주얼 디버거로 따라갑니다. 충돌 표시로 쓸데없이 복잡한 충돌 형상도 찾습니다. 줄이는 방법은 단순 충돌을 적극적으로 쓰고 복잡 충돌을 제한하며, Simulate Physics를 켠 물체의 수를 제한하고, 잠들기를 활용하며, CCD는 빠른 물체에만 쓰는 것입니다. 자세한 내용은 물리 시뮬레이션 성능 조정에서 다뤘습니다.
후처리
후처리는 ProfileGPU의 PostProcess 항목으로 비용을 봅니다. 쓰지 않는 후처리 효과는 끄고, 품질과 해상도, 샘플 수를 바꿔 다시 측정합니다. Intensity는 주로 겉모습의 세기라서, 낮추기만 해서는 비용이 줄지 않을 수 있습니다.
측정하고 고치는 흐름
성능을 분석하고 고치는 일은 다음과 같은 흐름으로 이어집니다. 먼저 프로젝트의 목표 플랫폼과 목표 FPS를 분명히 정합니다. 그다음 가장 복잡한 레벨이나 물체가 많은 곳처럼 문제가 생길 만한 곳에서 측정하고, Stat Unit과 ProfileGPU, 시각화 모드로 병목을 찾으며, CPU 바운드인지 GPU 바운드인지부터 가립니다. 측정한 데이터로 오버드로나 복잡한 셰이더, 너무 많은 물리 물체처럼 병목의 구체적인 원인을 찾고, 그 원인에 맞는 최적화를 적용합니다. 그리고 다시 측정해 나아졌는지 확인하고, 만족할 결과가 나올 때까지 되풀이합니다.
실무에서는 이 흐름을 좀 더 구체적인 순서로 진행합니다. Stat Unit으로 Game, Draw, GPU 가운데 어디가 프레임 예산을 넘는지 먼저 정합니다. CPU가 병목이라면 언리얼 인사이트에서 액터의 Tick, 물리, AI, 블루프린트 가운데 실제로 큰 항목부터 줄입니다. Draw가 병목이라면 그리기 호출과 컴포넌트 수, 재질 섹션 수를 보고, 계층형 LOD(HLOD)나 인스턴싱(instancing)으로 줄일 수 있는지 확인합니다. GPU가 병목이라면 캡처에서 큰 단계를 골라 오버드로, 그림자, 후처리, 반투명 가운데 해당하는 원인을 줄입니다. 최적화를 한 번 할 때마다 같은 카메라 위치와 같은 시나리오로 다시 측정해 나아진 폭을 기록하고, 목표 FPS에 닿으면 스케일러빌리티의 품질 단계마다 최소한의 기준을 지키는지도 확인합니다.
측정 값으로 먼저 조사할 곳 고르기
계산으로 연습해 봅니다. 아래 값은 판단을 연습하기 위한 측정 기록입니다. 같은 맵과 카메라, 해상도에서 Frame이 24 ms, Game이 8 ms, Draw가 10 ms, GPU가 23 ms라면, GPU가 가장 먼저 조사할 후보입니다. 이때 Game, Draw, GPU를 더한 41 ms를 프레임 시간으로 읽으면 안 됩니다. 세 축은 겹쳐서 실행되기 때문입니다. 실제로 측정한 Frame 24 ms를 FPS로 바꾸면 1000 ÷ 24 ≈ 41.7 FPS이고, 60 FPS의 예산인 약 16.67 ms를 넘습니다.
그다음 ProfileGPU로 큰 단계를 찾고, 그 설정 하나만 바꿔 다시 기록합니다. 그런데 GPU가 15 ms로 줄었다는 값 하나만 보고 끝났다고 판단하지 않습니다. 실제 Frame의 값과, 같은 구간을 되풀이할 때 가끔 생기는 긴 프레임, 화면 품질까지 확인해야 합니다. 측정할 때 프레임 제한이나 수직 동기화(VSync)를 켰는지도 기록합니다. 기다리는 시간이 길면, 처리 비용을 줄여도 Frame이 같은 값에 머물 수 있기 때문입니다.
기록해 둘 것
변경 전과 후를 비교하려면 같은 조건에서 같은 값을 남겨야 합니다.
| 기록 항목 | 같은 조건에서 남길 값 |
|---|---|
| 재현 조건 | 빌드, 장치, 맵, 카메라, 해상도, 품질 프리셋 |
| 병목 후보 | Frame, Game, Draw, GPU와 그 캡처 구간 |
| 변경 내용 | 한 번에 바꾼 설정이나 에셋과, 되돌릴 기준 |
| 다시 측정 | 프레임의 분포와 메모리, 화면 품질과 기능의 회귀 |
이렇게 기록해 두면 다음과 같은 질문에 답할 수 있습니다. Stat Unit의 결과를 스크린샷이나 로그로 남겨 전후를 비교했는지, 병목의 원인을 짐작이 아니라 ProfileGPU나 시각화 모드로 확인했는지, 최적화한 뒤 깜빡임이나 그림자 깨짐, 반사 품질 저하 같은 품질의 손실을 확인했는지, 저사양과 중간, 고사양 프리셋에서 같은 기능이 안정적으로 동작하는지, 그리고 팀 안에서 FPS와 메모리, 로딩 시간의 기준을 문서로 정해 두었는지입니다.
정리
- 성능 최적화는 개발 내내 측정하고 고치는 과정이며, 목표 플랫폼과 FPS를 먼저 정합니다.
- CPU는 게임 로직과 물리, AI, 그리기 호출의 준비를, GPU는 메시와 셰이더, 그림자, 후처리를 맡으며, 느린 쪽이 프레임을 제한합니다.
Stat Unit의Game,Draw,GPU는 겹쳐 실행되므로 더하지 않고, 가장 긴 축부터 판단하며, 60 FPS의 예산은 약 16.67 ms입니다.- CPU 쪽은 언리얼 인사이트로, GPU 쪽은
ProfileGPU로 깊이 보고, 시각화 모드의 색은 밀리초가 아니라 원인 후보를 찾는 단서입니다. - 메시, 재질, 조명, 파티클, 물리, 후처리마다 볼 도구와 줄이는 방법이 다르며, 후처리의
Intensity만 낮춰서는 비용이 줄지 않을 수 있습니다. - 한 번에 한 항목만 바꾸고 같은 조건에서 다시 측정하며,
Frame과 긴 프레임, 화면 품질, 프레임 제한 여부까지 기록합니다.
연습 문제
1. GPU만 줄이면 될까
프레임 제한 없이 같은 구간을 측정했더니 Frame이 20 ms, Game이 19 ms, Draw가 8 ms, GPU가 9 ms였습니다. 텍스처 품질을 낮춰 GPU를 5 ms로 줄이면 60 FPS를 달성할 수 있을까요? 다음에 어떤 축과 도구를 확인해야 할까요?
해설 보기
달성할 수 있다고 보장할 수 없습니다. Game이 19 ms로 이미 60 FPS의 예산인 약 16.67 ms를 넘으므로, GPU만 줄여서는 CPU 작업이 예산을 넘는 문제가 그대로 남습니다.
그래서 언리얼 인사이트의 타이밍 정보에서 같은 구간의 게임 스레드 호출을 보고, Tick이나 AI, 물리 같은 실제로 긴 작업을 찾아 줄입니다. 그다음 Frame의 분포까지 다시 측정합니다. 이 수치는 원인을 고르는 연습을 위한 조건이며, 실제 프로젝트의 측정 결과는 아닙니다.
2. 세 값을 더하면
팀원이 Game 8 ms, Draw 10 ms, GPU 23 ms를 더해 한 프레임에 41 ms가 걸린다고 보고했습니다. 같은 측정의 Frame은 24 ms입니다. 어느 값을 프레임 시간으로 봐야 하고, 왜 더하면 안 될까요?
해설 보기
프레임 시간은 측정한 Frame의 24 ms로 봐야 합니다. Game, Draw, GPU는 서로 다른 스레드와 장치에서 겹쳐서 동시에 실행될 수 있으므로, 단순히 더하면 실제보다 훨씬 긴 값이 나옵니다.
24 ms는 약 41.7 FPS에 해당하므로 60 FPS 예산을 넘고, 가장 긴 축인 GPU 23 ms가 먼저 조사할 후보입니다.
3. 줄였는데 Frame이 그대로
후처리 설정을 바꿔 GPU가 14 ms에서 10 ms로 줄었는데, Frame은 16.7 ms 그대로입니다. 측정할 때 수직 동기화가 켜져 있었습니다. 최적화가 효과가 없었던 걸까요?
해설 보기
꼭 그렇지는 않습니다. 수직 동기화가 켜져 있으면 프레임이 모니터의 주사율에 맞춰 기다리므로, 60 Hz 모니터라면 처리 비용이 줄어도 Frame이 약 16.7 ms에 머물 수 있습니다. 줄어든 GPU 시간만큼 기다리는 시간이 늘어난 것입니다.
그래서 효과를 비교할 때는 프레임 제한과 수직 동기화를 끈 상태로 측정하거나, 켠 상태라면 그 사실을 기록하고 GPU 같은 축의 처리 시간과 가끔 생기는 긴 프레임을 함께 봅니다.
성능 분석 도구는 Epic의 언리얼 인사이트 안내에서 확인할 수 있습니다.
다음 글 에셋과 레벨 최적화 기법에서는 측정으로 찾은 병목을 줄이는 구체적인 방법으로, 메시와 텍스처, 재질, 레벨 구조를 최적화하는 기법을 다룹니다.