정신없이 개발하다가 오랜만에 정보 공유겸 글 남김. 전에 글쓸때 유동은 뭐 업로드제한이 있길래 고닉도팠다.
우리게임 최근에 사람들 좀 모아서 비공개 테스트 (약 80명 규모) 진행했었는데, 발열 이슈가 생각보다 많이 올라와서 이번에 각잡고 최적화를 해보기로 했다.
최적화 관련해서 아는 사람들도 많겠지만, 초보자들을 위해 기본 개념부터 조금 설명하면서 시작함.
최적화를 통해 얻고자하는건 보통 시간상의 이득 (CPU/GPU점유율 감소)이나 공간상의 이득 (메모리 점유율 감소)이다.
이번에는 타임퍼포먼스를 챙기는 최적화. 그중에서도 GPU최적화를 다룰것임
목차
1.프로파일링
2. 유니티 프로파일러
3. 프레임 디버거
4. Batching
1. 프로파일링
최적화를 효과적으로 하기위해선 우선 프로파일링이 필요하다.
프로파일링은 어떤연산이 시간이 얼마나 걸리는지 파악해서, [가장 유의미한 성능향상을 가져올 수 있는 부분]을 찾아내는걸 말한다.
조금 다른 표현이긴하지만 [병목 지점]을 찾는다고 많이들 얘기한다.
유니티 Game View상단의 Stats버튼은 한번씩 눌러본 적 있을것이다. 여기서도 내 게임 성능에 대한 간단한 통계정보는 확인할 수 있다. 근데 이 기능은 프로파일링에 크게 도움되지않는다.
화면왼쪽아래 내가 Time.deltaTime으로 직접 찍은 FPS가 53인데, Statistics에는 111.3 FPS로 찍힌다. 이건 Stats창이 매우 간략한 정보만 표기 하기때문인데,
단순히 1초를 [CPU 메인스레드 연산에 걸린 시간]으로 나눈 수치를 표기한다. (1초 / 9.0ms ≒ 111.3FPS)
실제로는 Target FPS에 맞추기위해(수직동기화), 메인스레드가 대기하는시간이 있기때문에 실제로는 더 낮은 FPS이다.
그럼 뭘 써야 하느냐
유니티에서 프로파일링 하라고 만들어놓은 좋은 기능들이 있다.
바로 Profiler와 Frame Debugger다.
(유니티 외부 기능으로 더 깊게 프로파일링 할 수 있는 RenderDoc 이라는것도 있다. 관심있는사람은 검색해보길 권장. 이 글에선 다루지않음.)
유니티 상단메뉴 Window / Analysis / 에서 Profiler와 Frame Debugger를 확인할 수 있다.
2. 유니티 프로파일러
프로파일러를 열고 게임을 재생해보면 이런식 현란한 그래프들이 실시간으로 춤을 춘다.
마우스로 춤추고있는 그래프 아무곳이나 클릭해보면, 해당 시점의 상세한 정보를 아래에 표기해준다.
프로파일러는 무슨 연산이 어느정도 시간을 소요하는지 아주 상세하게 보여준다. 지금은 GPU최적화중이지만, CPU최적화할때도 맨날 켜서 보는것이다.
(프로파일러 상단의 Deep Profile 기능을 켜면 아주 상게하게 함수 콜스택까지 다 확인할 수 있다.)
우리는 GPU최적화할려고 켠거니까 렌더링 연산에 관련된부분을 열심히 마우스 휠 굴려서 확대해준다.
근데 이건 내 컴퓨터에서 에디터 플레이모드를 프로파일링 한 결과이므로, 도움은되겠지만 실제 모바일 환경에서의 프로파일링과 다를것이다.
단순히 PC와 모바일기기의 성능 차이로 인한 것이아니라, 모바일 프로세서와 PC 프로세서의 구조 차이로인해 각자 유리한연산, 불리한 연산이 존재한다.
(대표적으로 PostProcessing의 Bloom의 경우, 모바일프로세서에서 구조상 특히 불리한 연산이라 성능이 조져지는걸 볼 수 있다)
그럼 Profiler를 폰에 연결해보자.
폰에 프로파일러 연결하는법은 구글링하면 정보가 많을텐데, 간략하게 설명하면 다음과같다.
1. 안드폰을 준비 (IOS는 안해봐서 모르겠따)
2. 개발자옵션에서 USB 디버그를 허용한다
3. 빌드할때 Build Setting의 Development Build 옵션 체크
4. 폰을 컴터랑 USB로 연결한담에 BuildAndRun으로 빌드
위의 과정을 다 거치고 폰에 내 게임이 켜지고나면, 여기 프로파일러에 내 폰 정보가 뜰것이다. 그걸 누르면 PC에서랑 똑같이 프로파일러를 사용할 수 있다.
이제 빨간부분을 보면, 대충 상황파악을 할 수 있다.
PostProcessing이 전체 연산 시간의 3분의1 이상을 쳐먹고 있는 미친 상황을 볼 수 있다 (나는 PP스택에 Bloom하나만 넣었다. 시벌탱)
이 결과는 Bloom을 걍 빼버리면 성능이 대폭 개선될거라는 의미이므로, 과감하게 제거해버리는 판단도 나쁘지않다.
Bloom 없음(왼쪽), 있음(오른쪽)
킹치만 블룸은 존나 멋있기때문에, 어떻게든 살려서 가져가보려고 지금부터 똥꼬쇼를 할것이다.
현재 내 프로젝트는 URP를 사용하고있고, 포스트프로세싱은 유니티에서 URP와 함께 사용하라고 권장하는 URP Integrated PP를 쓰는중이다 (PPv3이라고도 부르는거 같다)
보통 Bloom에는 radius를 조절하는 파라미터를 건드려서 iteration횟수를 줄이거나, 해상도를 다운샘플링해서 비주얼과 성능사이의 타협이 가능하다.
근데 미친유니티가 URP Bloom에 성능 타협옵션을 안만들어놨다.
바로 구글링 들어간다.
검색하자마자 유니티 포럼에 바로 관련 정보가 뜨는걸보면, 나랑 같은 문제로 고통받는 개발자들이 많은것으로 추정된다.
글 내용은 대충, 모바일에서 URP Bloom이 너무 양심없이 느리다는것인데
해결방법에대한 토의를 하는중인거같다 읽어보자
유니티 공식 답변 : '저도 봤는데 미쳤다고 생각합니다'
??아니 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
아무튼 그래서 스레드를 쭉 따라 읽어보니까, '성능 좋게하겠다고 파라미터 조정하면 퀄리티가 조져질겁니다' 등의 얘기를하면서 혓바닥이 길어지는걸로 보아 유니티의 정식 지원은 늦어질 것 같다. (저 글이 2019년도 글인데, 현재에도 Bloom옵션타협은 개발브랜치 상태로 존재하는걸 확인했다. https://github.com/Unity-Technologies/Graphics/tree/universal/bloom-quality-settings)
하지만 다행히 URP같이 UPM을 통해 받은 패키지들은 내가 직접 코드를 수정할 수 있다. 유니티 프로젝트의 Library폴더안에 보면 PackageCache라는 폴더가 있는데, 여기에 내 패키지들이 다 들어있다.
포스트프로세싱 Bloom의 파라미터를 조정할 수 있는곳은
\Library\PackageCache\com.unity.render-pipelines.universal@7.5.3\Runtime\Passes\PostProcessPass.cs
이다.
(주의할점 : PackageCache는 유니티 프로젝트를 로드 할 때마다 무결성체크후 Re-import를 하므로, 코드를 수정해도 플젝을 다시 열면 원상복귀된다. 이걸 방지할려면 Package폴더의 manifest.json에 해당 패키지 경로를 내 로컬파일경로로 수정해주는 작업을 해줘야함)
코드 열어서 읽어보니까 iteration횟수를 const값으로 박아놓은걸 볼 수 있다. 변수이름에 Max가 들어간걸로보아 최대값을 const로 박아놓고, 조절가능한 옵션을 달아주려 한거같은데 아직 개발이 안된 상태다. 지금은 내가 이걸 수동으로 조정하면 된다
사실 Bloom은 3D게임에서 아주 넓은 고-급 빛번짐 효과를 주는 용도로 사용하기도하지만,
우리플젝은 그냥 쪼만한 2D게임에서 스프라이트에 glow효과 살짝 넣는거라 iteration을 줄인다고해서 크게 문제없을것이다.
과감하게 파라미터를 확 낮춰버린 결과
비포(위) 애프터(아래)
성능개선이 크게 이루어진 것을 볼 수 있다. 포스트프로세싱이 전체 렌더링에서 차지하는 비중을 살펴보면 엄청난 결과다
3. 프레임디버거
프레임디버거는 한 프레임이 렌더링 될 때, 어떤 순서대로 어떤 과정을 거쳐서 렌더링되는지 보여주는 기능을 제공한다.
게임을 재생한 상태에서 프레임 디버거를 활성화하면, 일시정지모드로 변하면서 그 장면을 어떻게 렌더링하는지 볼 수 있다.
(프레임디버거로 보는 정보는 PC나 모바일이나 똑같아서 굳이 폰 연결이나 그런거 안하고 에디터에서 확인하면 된다)
프레임디버거에 뜨는 연산이름들을 프로파일러에서 그대로 확인할 수 있다.
프레임디버거와 프로파일러를 같이 사용하면, 무슨 렌더링할 때 시간이 얼마나 걸리고 있는지 알 수 있는것이다.
이쯤에서 GPU에대한 설명을 잠시 하고 넘어가야할 것 같다.
프레임 한장이 렌더링될 때, CPU는 렌더스레드에서 GPU로 렌더링에 필요한 정보를 넘겨주고 렌더링 연산요청을 한다.
(이 때, 넘기는 정보를 RenderState, 연산요청을 드로우콜, DP콜 등으로 부른다)
CPU가 화면에 렌더링할 정보를 GPU한테 넘겨서 렌더링연산 외주를 맡기는거라고 이해하면 편하다.
(CPU는 적은수의 고-급 프로세서를 갖고있고, GPU는 값싼 프로세서를 수백~수천개 갖고있는 장치다. 그래서 렌더링연산같이 [병렬처리하면 효율이 쌉좋은 연산]들을 GPU한테 외주맡기는것이다.)
여기서 최적화할 포인트를 하나 잡을 수 있다.
드로우 콜 횟수를 줄이는것이다.
RenderState를 바꾸는 작업은 결국 CPU메모리에서 VRAM으로 데이터를 옮기는 작업이고, 컴구나 OS좀 공부한사람이라면 알겠지만 이런 작업에 드는 커뮤니케이션 오버헤드는 상당한 편이다.
이 오버헤드를 줄이기 위해, 같은 RenderState를 가진연산을 묶어서 드로우콜을 보내는 최적화방법을 사용하는데
이걸 Batching이라고 그런다.
4.Batching
배칭은 고-전적인 Static Batching, Dynamic Batching같은것도 있지만
요즘 유니티에서 밀고있는 SRP Batching이라는게 있다. 난 이걸 쓸거다.
https://docs.unity3d.com/Manual/SRPBatcher.html
표준 Batcher (왼쪽), SRP Batcher (오른쪽)
SRP배쳐는 유니티가 야심차게 내놓은 기능답게 아주 파워풀하다.
기존 배쳐는 여러 오브젝트들을 렌더링할때, 오브젝트들이 같은 Shader를 쓰더라도 다른 Shader Property이면, 성능이 많이드는 재설정 작업을 거쳐야했다. 즉, 같은 쉐이더 쓰는 다른 Material들을 여러개 쓰면 성능이 조져진다는것이다.
근데 우리의 갓-SRP배쳐님 께서는 Shader가 같으면 매터리얼이 달라도 성능하락을 크게 막아주신다.
현업에서 일하는 이펙터들도 Material의 Color Tint를 조금씩 변경해서 여기저기 집어놓는 개짓거리를 하는걸 봤는데, 그런 상황에서 프로그래머의 혈압수치를 더이상 올리지 않아도 된다는 의미이므로 아주 훌륭한 기능인것이다.
아무튼, 배칭을 위해서는 렌더링될 오브젝트들을 같은 RenderState로 만들어주면 되는데
같은 쉐이더 (되도록 같은 매터리얼인게 더 좋음), 같은 텍스쳐를 사용해 렌더링하도록 해주면된다.
여기서 '같은 텍스쳐를 사용해' 부분 에서 먼 개소린가 싶을 수 있다.
나는 칼이랑 방패 렌더링하고싶은데 이거 두개 텍스쳐를 똑같이 만들라고??
그렇다. 칼이랑 방패를 같은이미지에 나란히 넣어놓고, uv좌표만 다르게해서 원하는부분만 각각 짤라서 렌더링하면 같은 텍스쳐로 두개의 오브젝트를 렌더링하는것이다.
이 때 쓰는게 Sprite Atlas이다.
Project창에 우클릭/Create/Sprite Atlas 해서 생성하면된다.
우리 게임에 쓰이는 타일들을 하나의 아틀라스로 패킹했다.
배칭을 적용하기전에는 일일이 하나하나 드로우콜이 들어간 반면에
아틀라스로 묶어서 배칭한 결과 한방에 렌더링되는걸 볼 수 있다.
타일과, 장식물들의 드로우콜이 나뉘는 이유는, 타일은 TileMapRenderer 장식물은 SpriteRenderer를 쓰기 때문인데
이 둘을 같은 드로우콜에 배칭하는방법은 아직 모르겠다. 뭐 이정도만해도 크게 성능 향상이되었을 것이니 다른 쪽에 신경 쓰는게 더 좋을 것 같다.
(참고 : 타일맵 렌더러는 Mode를 Individual이 아닌 Chunk로 설정해줘야 하나의 드로우콜로 묶인다)
이런식으로 화면에 동시에 렌더링될 리소스들을 아틀라스로 패킹해서 최적화를 진행해준다.
UI를 아틀라스로 패킹할 때 주의할점
아틀라스 옵션에서 Allow Rotation과 Tight Packing을 체크해제 해주어야한다.
SpriteRenderer같은걸로 렌더링할 때는 상관없는데 UI Image로 렌더링할 때 저 옵션 켜서 패킹하면 영 좋지않은 모양새를 볼 수 있는데,
이건 스프라이트랑 UI Image랑 렌더링하는 방식이 다르기때문이다.
UI는 무조건 버텍스 4개짜리 메쉬를 고정으로 박아놓기때문에,
TightPacking을 사용하면, 같이 아틀라스에 묶여있는 다른 이미지들이 삐져나와보일 수 있고
AllowRotation을 사용하면, uv가 고정이므로 회전이 이상하게 꼬여있는걸 볼 수 있다.
아무튼 이렇게 Batching을 통해서 드로우콜을 줄이면,
게임뷰 Stats를 통해서, Batching으로 인해 얼마나 드로우콜이 줄었는지 확인할 수 있다.
그리고 프로파일러를 돌려보면
3.03ms에서 2.67ms 로 매우 유의미하게 성능향상이 된 걸 볼 수 있다.
아까 Bloom이 워낙 미친놈이라서 성능 향상이 적어보이는 착각이 들지만 이정도면 아주 훌륭하다.
----
최적화 할려고 시도했던거는 많은데, 대충 굵직한것들만 적어봤음.
시간이랑 여유가 더 있다면 다음에도 또 썰풀거 갖고올게
덧글로 질문남기면 아는선에서는 다 답해줌
꿀팁추
유익한 정보 땡큐. 조만간에 나도 최종 최적화 한번 볼 거 같은데 그 때 글 참고하면 되겠다.
이 글은 개발일지로 쓴거라 설명이 아주 자세하진 않은데.. 아조씨 경기게임오디션 top10부터는 소통하는 네트워크 생긴다고 하니까 거기서 볼 기회있으면 직접 뭐 물어봐도 좋아요. 주모키우기 꼭 잘됐음 좋겠다
와 진짜 고급지다... 진짜 유익한 정보네 이거. 나도 꼭 이거 사용해봐야겠어~
1. 최소 사양 폰은 어너정도로 잡고 있나요? 2. UI를 아틀라스로 만들때하고, 안만들때 하고 차이가 커나요? 3. 스프라이트 같은걸 아틀라스로 만들어 버리면... 나중에 Resource<>로더 같은 걸로 읽어와야할때 어떻게 읽어오나요? 아틀라스로 만들어 버리면 이름을 잃어 버려서 찾을수가 없던데.... 여러가지 질문 감사합니다. ^^*
ㅇ
이거 나도 궁금하네
1. 우리게임은 오브젝트가 많은것도아니고, 2d인데다가 물리연산도없어서 (턴제겜이라 반속 중요도도 낮음) 단종된 폰 (2014년 기종 아이폰6정도)에서도 돌리는 것을 목표로 하는중. 타겟팅할때는 프로세서보다는 메모리를 기준으로 하는데, 프로세서가 구리면 프레임이 떨어질 뿐이지만, 메모리를 초과하는순간 os가 앱을 종료시키기때문. 물론 목표일뿐 가능할진 모름
2. 사실 차이가 크다 작다는 상대적인거라 뭐라말하긴 힘들지만, 무조건 유의미한 차이가 있음. 그리고 아틀라스로 만드는쪽이 이미지크기를 pot(2제곱수)로 알아서 패킹해주니까 용량 압축에도 좋아서 사실 안할 이유가 거의없는 편. 근데 최적화는 하면 무조건 좋긴하지만, 성능이슈가 생기지않는다면 다른쪽에 더 신경쓰는게 더 효율 좋을 수 있음
3. Resources.Load는 사실 유니티에서 버린기능임. 유나이트같은데 가봐도 Resources.Load 안쓰는솔루션을 계속 얘기하고, 유니티 관계자한테도 최대한 쓰지말라는 얘길들음. 이건 Resources.Load에 필요한 LUT(룩업테이블)과 관련된 성능이슈때문임. 대충 쉽게설명하면 Resources 폴더에 뭐가 많아질수록 성능이 점점 나빠진다는
건데, 그럼 Resources.Load의 대체재는 무엇이냐하면, SerializeField에 드래그 드롭으로 할당해서 쓰거나, Addressable을 사용하는것임. 물론 시리얼라이즈필드에 전부 올려두면 메모리에 다 올라가니까 원치않는 결과일 수 있음. 그럴땐 ScriptableObject를 만들어서 거기에 리소스들을 드래그해 넣고,
스크립터블 오브젝트를 Resources.Load로 불러오면됨. 한번에 리소스 뭉치를 로드하는거니까, Resources폴더도 내용물이 많지않고 io횟수도 적음. 그리고 무엇보다도, 님이 궁금해하는 아틀라스 패킹시 로드 불가능한 문제도 없어짐. 유니티 시리얼라이즈 필드를 이용하는것이므로, 아틀라스로 패킹해도 리소스 연결이 끊기지않음. 유니티에서 권장하는 방식임
사전지식없이 쉽게 가능한건 ScriptableObject사용하는 방식인데, 시간적으로 여유가 있다면 Addressable을 찾아서 공부해보는것도 매우 추천함.
개씹꿀팁 ㄷㄷ
다 잘했는데 냉정하게 봤을 때 Bloom 은 빼는게 맞을듯... 저정도 연출 효과 보려고 성능 감수하면서 넣는다는건 솔직히 고집 아닐까. 나도 작년에 URP 처음달고 블룸 떡칠해서 눈뽕연출 오지게 넣었다가 출시 전에 다뺌 ㅋㅋ; 암튼 화이팅
ㅋㅋㅋㅋㅋㅋㅠㅠㅠㅠㅠ
왤케 유익함
오 이걸 이제 봤네 ㄱㅅㄱㅅ 적용해봐야겠다
그냥 블룸 적용되어있는 sprite를 만드는 것도 좋을 듯
와 이거 신박하다고 생각해서 나름 재밌게 했었는데 제작자가 여깄네