Skip to content

모바일 게임 성능 최적화를 위한 일반적인 기법

🌐 원문 링크: 移动游戏性能优化通用技法 (cnblogs.com/timlly)
📅 원문 발행일: 2019-03-12 | ✍️ 저자: Timlly (00 / )

💡 시리즈 분류: Mobile Optimization국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료


수년 전, 이 수년간의 작업에서 축적된 최적화 경험에 대한 기사를 쓰고 싶었지만 너무 게을러서 하지 못했습니다. 최근에 드디어 글을 쓰게 되었습니다.

특정 최적화 기술과 관련하여 블로거는 독자들이 무슨 일이 일어나고 있고 왜 일어나는지 알 수 있도록 원칙과 기초를 설명하기 위해 최선을 다할 것입니다. 이 기사를 완전히 이해하려면 독자는 컴퓨터 언어/그래픽/게임 엔진에 대한 특정 기초를 갖추어야 합니다. 이 글을 읽은 후 독자들이 게임 성능을 일정 수준까지 최적화하여 게임의 효과와 효율성이 고/중/저사양 기기에서 제품 기대치를 충족하거나 초과할 수 있기를 바랍니다.

설명을 용이하게 하기 위해 아래의 많은 현지 블로거들은 이전에 Xishanju에서 진행된 RTS 모바일 게임 프로젝트(Game Z라고 함)를 예로 사용할 것입니다. Unity를 예로 들면 다른 상용 엔진이나 자체 개발 엔진에 적용할 수 있습니다.

1.1 성능 최적화의 중요성

게임을 해본 사람들은 종종 다음과 같이 한탄할 것입니다. 게임이 왜 이렇게 정체되는 걸까요? 전화기가 왜 이렇게 뜨거워? 배터리가 왜 이렇게 빨리 소모되나요?

사실 이러한 문제는 모두 성능 문제에 기인하며, 게임에 있어서 성능 최적화의 중요성은 자명합니다.

신규 게임 프로젝트의 경우, 중·초기에는 수요와 효과에 급급한 채 성능 최적화 작업을 소홀히 했을 수도 있습니다. 중간 및 후반 단계에서는 성능 병목 현상이 불가피하게 두드러지게 됩니다. 초기 단계에서 리소스를 효과적으로 구성하고, 아트 사양을 공식화하고, 개발 프로세스와 도구 체인을 개선할 수 있다면 이후 단계에서 최적화하는 것이 훨씬 쉬워지고 많은 우회를 피할 수 있습니다.

1.2 최적화 팁

수년 전, 대학을 갓 졸업한 한 블로거는 그래픽 업계의 한 위대한 인물이 다음과 같이 말하는 것을 들었습니다. 최적화에는 끝이 없으며 최적화의 가장 높은 상태는 렌더링이 없는 것입니다.

나는 그가 이전에 말한 내용을 완전히 이해할 수 없었고 조금 과장된 것이라고 생각했습니다. 그러나 블로거는 여러 게임 프로젝트에서 성능 최적화 작업을 수행한 후 깊은 이해를 갖게 되었습니다. 오랜 세월이 지난 후에도 그의 말이 여전히 그의 귀에 울리고 있으며, 진정한 향기의 법칙이 성취되었습니다!

최적화의 본질은 렌더링하지 않음, 렌더링 감소 또는 더 경제적인 방법으로 렌더링하는 것입니다.

1.2.1 우선순위 구별

성능을 최적화하려면 먼저 성능 병목 현상을 식별하고 성능에 가장 큰 영향을 미치는 영역을 먼저 최적화한 다음 영향이 가장 적은 영역을 최적화하는 등의 작업을 수행해야 합니다. 이 규칙을 따를 수 있는 경우 최적화 효과와 소요 시간 간의 곡선 관계는 대략 다음과 같습니다.

즉, 초기 단계에서는 최적화 효과가 매우 뚜렷해지겠지만, 시간이 지날수록 점점 더 많은 시간이 소요되고 최적화 효과는 점차 둔화됩니다. 이는 우리에게 다음을 알려줍니다.

  1. 먼저 성능 병목 현상을 식별하면 최적화 효과가 가장 뚜렷해집니다.

  2. 최적화에는 끝이 없습니다. 이후의 최적화 효과와 시간 비율이 감소하므로 적당히 중지하는 것이 좋습니다.

1.2.2 분석 도구를 잘 활용하세요

일꾼이 일을 잘하고 싶다면 먼저 도구를 갈고 닦아야 합니다. 분석 도구를 잘 활용하면 성능 병목 현상을 신속하게 찾아내고 절반의 노력으로 두 배의 결과를 얻을 수 있습니다. 성능 분석 도구에 대해서는 다음을 참조하십시오: 1.3 보조 도구

1.2.3 성능과 효과의 균형

게임마다 중점을 두는 부분이 다릅니다. 일부 게임은 효과에 중점을 두지만 성능에는 그다지 중점을 두지 않습니다. 일부 게임은 효과를 희생하면서 효율성에 중점을 둡니다. 일부 게임에는 두 가지가 모두 필요합니다.

블로거의 경험에 따르면 대부분의 게임 개발자나 플레이어는 성능에 더 중점을 둡니다. 예를 들어, 대부분의 사람들은 배틀 로얄 게임을 할 때 프레임 속도를 안정화하고 전력 소비를 느리게 하기 위해 모든 이미지 품질 매개변수를 가장 낮은 설정으로 전환합니다.

성능상의 이유로 인해 모든 효과가 삭제될 수 있습니다.

1.2.4 맞춤형 최적화 참조 지표

각 프로젝트를 최적화하기 전에 고급형, 고급형, 저가형 시스템이 실행될 장치, 달성할 프레임 속도 등과 같은 특정 표시기 매개변수를 사용자 정의해야 합니다. 게임에서 설정하는 참조 표시기는 다음과 같습니다.

이 표준은 2017년 상반기에 설정되었으며 이제 매개 변수를 적절하게 완화할 수 있습니다. 기준을 설정한 후 이를 바탕으로 최적화가 진행됩니다. 매개변수 표시기에 도달하면 최적화 작업이 완료된 것으로 간주될 수 있습니다.

1.2.5 효과 수준 관리를 잘 수행

레벨 관리 로직을 별도의 모듈로 추상화하고, 레벨 관련 로직의 전반적인 관리와 구현을 담당하는 QualityManager 역할을 추가하는 것이 좋습니다. 다른 모듈에서는 이 모듈의 인터페이스만 호출하면 차별화된 로직을 쉽게 구현할 수 있습니다. 또한 QualityManager는 주요 게임 매개변수(fps/네트워크 핑 값/트래픽)를 수집하고 외부 쿼리 인터페이스를 제공할 수 있습니다.

1.2.6 특정 상황에 대한 상세 분석

각 게임 프로젝트의 구체적인 상황은 다르므로 이 문서에 포함된 매개변수와 방법은 기계적으로 복사할 수 없습니다. 그렇지 않으면 비생산적일 수 있고 시간이 걸리며 성능을 제대로 향상시키지 못할 수도 있습니다.

1.3 보조 도구

현재 시장에는 다양한 기능과 적용 가능한 플랫폼을 갖춘 많은 분석 도구가 있습니다. 다음은 일반적으로 사용되는 도구에 대한 간략한 소개입니다. 구체적인 사용방법은 별도로 검색해 주세요.

1.3.1 엔진 분석 도구

  • Unity 프로파일러

Unity 프로파일러는 CPU/GPU/메모리/오디오/물리/네트워크 및 기타 모듈의 특정 소비 매개변수를 볼 수 있습니다. Unity 게임에 필수적인 성능 분석 도구입니다.

  • 언리얼

Unreal 3 및 이전 버전의 경우 먼저 명령줄을 사용하여 프로파일러 파일을 생성한 다음 생성된 파일을 UnrealFrontend를 통해 로드하여 소비량을 확인해야 합니다. Unreal 4는 Unity의 프로파일러와 유사한 창을 제공하므로 더욱 편리합니다. 더 보기: 언리얼 프로파일러 툴 레퍼런스

1.3.2 IDE 분석 도구

  • Visual Studio 성능 프로파일러

VS는 Windows 플랫폼에서만 실행될 수 있습니다. 현재 프로젝트/지정된 exe/실행 프로세스의 CPU 및 GPU 성능을 샘플링하고 특정 소비 지표를 볼 수 있습니다.

  • XCode 도구

XCode는 Mac OS에서만 실행될 수 있으며 Mac OS 및 iOS 앱의 CPU/GPU/메모리 및 기타 성능 매개변수를 디버깅할 수 있습니다.

  • Android 스튜디오 프로파일러

Android Studio는 Windows 및 Mac OS에서 실행할 수 있지만 Android 앱만 분석할 수 있으며 매개변수에는 CPU/메모리/네트워크 등이 포함됩니다.

1.3.3 GPU 공급업체 도구

거의 모든 주요 GPU 제조업체는 자체 제품 디버깅을 위한 GPU 분석 도구를 제공합니다. 엔진 및 IDE 도구와 가장 큰 차이점은 렌더링 상태/참조 리소스/각 그리기 호출의 그려진 그림은 물론 기타 고유 매개변수를 볼 수 있다는 것입니다.

  • 테그라 그래픽 디버거(NV)

도구 자체는 다양한 주류 시스템에서 실행될 수 있고 OpenGL 및 Vulkan도 분석할 수 있지만 NV의 Tegra K1 및 X1 시리즈 GPU만 지원합니다.

  • 말리 그래픽 디버거(Arm)

Tegra Graphics Debugger와 유사하지만 다양한 버전의 OpenGL을 완벽하게 지원하고 Vulkan 및 OpenCL 디버깅도 지원합니다.

  • PVRTrace(상상 기술)

Imagination Technology의 GPU를 사용하는 앱만 디버깅할 수 있습니다.

  • Adreno 프로파일러(Qualcomm)

Qualcomm GPU를 사용하는 앱만 디버깅할 수 있습니다.

  • PerfHud(NV)

PC 프로그램만 지원하고 매우 강력한 GPU 디버깅 도구이지만 모바일 장치를 디버깅할 수 없으며 에뮬레이터에 의존해야 합니다.

  • PerfHud ES(NV)

PerfHud 모바일 버전은 모바일 장치 디버깅을 지원하며 전체 CodeWorks for Android 개발 패키지를 다운로드해야 합니다.

1.3.4 기타 타사 도구

  • PIX(MS)

Windows 플랫폼에서만 실행되며 DirectX 분석만 지원합니다.

2. 리소스 최적화

질병은 입을 통해 들어오고, 자원은 입구와 같다. 문제가 있으면 일련의 성능 문제가 발생합니다. 반대로 리소스가 잘 최적화되면 이후의 모든 챕터의 성능이 향상될 수 있습니다. 리소스 최적화 장이 먼저 언급되는 이유이기도 합니다.

2.1 텍스처 최적화

텍스처 최적화의 목적은 텍스처가 차지하는 메모리를 가능한 한 작게 만드는 것입니다. 텍스처가 메모리에 로드된 후 크기 계산 공식은 다음과 같습니다.

\begin{배열}{l} \text{텍스처 메모리 크기(바이트)} &=& \text{텍스처 너비} \times \text{텍스처 높이} \times \text{픽셀 바이트} \\ \text{픽셀 바이트} &=& \text{픽셀 채널 수(R|G|B|A)} \times \text{채널 크기(1바이트|니블)} \end{배열}

위 공식에서 볼 수 있듯이, 텍스처가 메모리에 로드된 후의 텍스처 크기는 크기/픽셀 채널 수/채널 크기와 관련이 있으며, 우리는 이들로부터 최적화를 시작합니다. 또한 재사용률을 높이고 아틀라스를 합성하여 최적화를 달성할 수도 있습니다.

2.1.1 텍스처 크기

아티스트가 저지르는 일반적인 실수는 캐릭터나 장면이 무엇이든 일반적으로 1024x1024보다 큰 큰 크기의 텍스처를 모델에 첨부한다는 것입니다.

예를 들어 Game Z는 45도 경사 고정 시점의 전투 게임입니다. 캐릭터 렌더링은 매우 작은 화면(약 150x150)을 차지하지만 아티스트는 처음에 모든 캐릭터 모델에 대해 1024x1024 텍스처를 제작했습니다. 프로젝트의 실제 상황에 따라 블로거는 모든 캐릭터 텍스처를 256x256으로 줄이고, 캐릭터 텍스처 점유율을 1/16으로 줄였습니다. 캐릭터 텍스처 외에도 무기/장비/특수효과/장면 등 관련된 모든 텍스처가 적절한 크기로 축소되었습니다. 여기서 적절한 크기는 그림에서 대부분의 경우 렌더링 개체가 도달할 수 없는 최대 크기를 나타냅니다. 이 크기를 2의 N승으로 유지하는 것이 가장 좋습니다.

2.1.2 텍스처 채널

채널 최적화의 목적은 픽셀 크기를 줄이는 것입니다. 이는 다음 방법을 통해 달성할 수 있습니다.

  • 알파 채널을 제거합니다. 채널 수를 줄일 수 있어 알파 블렌딩이나 알파 테스트가 필요하지 않은 캐릭터 및 개체에 적합합니다.

  • 단일 채널 플롯을 적용합니다. 회색조 이미지, 지형 높이 맵, 마스크 이미지, 셰이더 마스크 이미지 등과 같은 채널 수를 줄일 수도 있습니다.

  • 32비트 이미지 대신 16비트 이미지를 사용하십시오. 예를 들어 RGB444/RGBA4444는 픽셀 채널 크기를 줄일 수 있습니다.

  • 압축된 텍스처는 다양한 플랫폼에 적용됩니다. 예를 들면:

Windows는 DDS로 압축할 수 있으며, DDS는 DX1~DX5의 5가지 형식으로 세분화됩니다. 각 응용 프로그램 시나리오는 약간 다르지만 DirectX에만 사용할 수 있습니다.

  • Android는 ETC1(알파 없음) 또는 ETC2(알파 있음)로 압축할 수 있습니다.

  • iOS는 PVRTC 형식으로 압축할 수 있습니다.

모두 GPU에서 직접 지원하는 텍스처 형식이므로 메모리/비디오 메모리/대역폭 사용량을 크게 줄일 수 있습니다.

  • JPG/고압축 PNG/GIF와 같은 저품질 형식을 사용하지 마세요. 현재 주류 상용 엔진은 게임 출시 과정에서 모든 텍스처를 자동으로 압축하기 때문에 원본 품질의 텍스처를 유지하면 텍스처 압축 후 이미지 품질 손실을 줄일 수 있습니다.

2.1.3 텍스처 재사용률 향상

다음 방법은 텍스처 재사용률을 향상시킵니다.

  • 공유 갤러리를 만듭니다. 버튼/진행 표시줄/배경/UI 공통 요소 등과 같은 공통 요소를 공유 라이브러리에 넣습니다.

  • 큰 배경 이미지를 9개의 정사각형 그리드 이미지로 교체합니다. Jiugongge는 게임 개발에서 비교적 일반적인 UI 구성 요소입니다.

  • 텍스처 요소는 변환을 통해 복합 텍스처로 결합될 수 있습니다. 예를 들어 아래 그림에서는 동일한 텍스처 인스턴스 4개를 회전/뒤집으면 상하좌우 대칭인 배경 이미지를 얻을 수 있습니다.

  • 지우공게 + UI 요소를 결합하면 매우 복잡하지만 상대적으로 저렴한 UI 인터페이스로 결합할 수 있습니다.

2.1.4 텍스처 아틀라스

아틀라스는 작은 크기의 텍스처 요소 묶음으로 구성된 텍스처 맵입니다(아래 참조).

아틀라스는 IO 로드 및 그리기 호출 수를 줄일 수 있지만(자세한 내용은 4.2 배치 참조) 다음과 같은 부작용도 있습니다.

  1. 기기가 지원하는 최대 크기를 초과할 수 있습니다.

  2. 아래와 같이 큰 공백 픽셀이 나타날 수 있습니다.

부작용 1의 경우, 아틀라스의 최대 크기를 제한하고(보통 2048x2048 이하) 이를 여러 아틀라스로 분할할 수 있습니다. 부작용 2의 경우 합성된 아틀라스가 가능한 한 많은 유효 픽셀을 차지하도록 텍스처 요소의 레이아웃이나 크기를 원하는 방식으로 조정할 수 있습니다.

아틀라스 생성에 적합한 리소스에는 UI 인터페이스, 소품 아이콘, 캐릭터 아바타, 스킬 아이콘, 시퀀스 프레임, 특수 효과 등이 포함됩니다.

Unity 엔진인 경우 SpritePacker를 사용하여 아틀라스를 쉽게 생성하고 미리 볼 수 있습니다. 자체 개발한 엔진이라면 TexturePacker의 Command Line Tool을 이용하여 합성할 수 있습니다.

2.2 UI

2.2.1 UI 아틀라스

모든 UI 요소는 아틀라스를 생성하고 각 인터페이스가 별도의 아틀라스를 생성하는지 확인하여 인터페이스가 소멸될 때 UI 텍스처가 제때에 해제될 수 있도록 합니다. 각 인터페이스가 자신의 아틀라스와 공유 라이브러리 아틀라스만 참조하는지 확인하고 다른 인터페이스의 아틀라스는 참조하지 않도록 하세요.

위 그림과 같이 인터페이스 A가 인터페이스 A 아틀라스와 공유 아틀라스를 참조하는 것은 허용하되 인터페이스 B 등 다른 아틀라스는 참조하지 않도록 노력합니다. 그러나 실제 게임 개발 과정에서는 아트가 이를 달성할 수 있는지 보장하기 어렵습니다. 일반적으로 다음과 같은 문제가 존재합니다.

  • 인터페이스 A가 실제로 인터페이스 B의 아틀라스에 있는 요소를 사용해야 하는 경우 어떻게 해야 합니까?

참조 솔루션: 참조된 요소의 다양성에 따라 달라집니다. 인터페이스 A와 B에서만 사용되는 경우 참조된 요소를 인터페이스 A의 앨범에 복사할 수 있습니다. 다른 인터페이스에서도 참조되는 경우 공유 갤러리로 이동할 수 있습니다.

  • 일부 UI 텍스처는 매우 크고 많은 인터페이스에서 사용됩니다. 공유 갤러리에 배치하면 공유 갤러리가 빠르게 확장됩니다. 어떻게 해야 하나요?

참고 솔루션: 대형 텍스처의 경우 9각형 그리드 + 상세 도면을 사용하거나 조합을 통해 교체하는 것이 좋습니다.

  • 예술 제작 UI가 자체 아틀라스와 공유 아틀라스만 참조하도록 하려면 어떻게 해야 합니까?

참조 솔루션: 일괄 검사 도구를 구현하여 각 UI 인터페이스에서 참조하는 아틀라스 목록을 찾습니다. 2개 이상의 아틀라스가 참조되면 부적격입니다.

2.2.2 UI 수준

UI 요소가 많기 때문에 주류 상용 엔진은 Draw Call을 줄이기 위해 이를 일괄 처리하지만 일괄 최적화는 조건부입니다.

  1. 동일한 재료를 사용합니다. 엔진의 기본 UI 머티리얼 + UI 아틀라스를 사용하면 이 조건을 충족할 수 있습니다.

  2. 그리기 순서는 연속적입니다. UI의 그리기 순서는 일반적으로 장면의 노드 순서입니다.

아래 사진에는 4개의 UI 그림이 있는데 모두 시스템 기본 재질을 사용하고 공유 아틀라스의 모든 요소이며 장면 내 순서도 연결되어 있으므로 일괄 최적화 조건을 충족하며 최종 SetPass 호출(Draw Calls)은 1입니다.

그러나 실제 UI를 만드는 과정에서 UI 일괄 최적화를 위한 조건이 파괴되는 경우가 많습니다.

  • 동일한 인터페이스가 종종 자체 아틀라스와 공유 아틀라스를 참조하여 최적화 조건 1을 파괴합니다.

  • 인터페이스에는 일반적으로 텍스트가 있으며, 텍스트는 일반적으로 엔진에 의해 자동으로 생성된 또 다른 하나 또는 여러 개의 그림으로, 이는 최적화 조건 1을 파괴합니다.

  • UI 노드 계층 구조가 혼란스럽고, 서로 다른 아틀라스의 요소가 서로 교차하여 최적화 조건 2를 파괴합니다.

  • 일부 UI 요소는 최적화 조건 1을 파괴하는 커스텀 머티리얼이나 셰이더를 사용합니다.

이유를 분석한 후에는 UI를 만들 때 이러한 상황이 발생하지 않도록 최선을 다해야 합니다.

2.2.3 기타 UI 최적화

  • MipMap을 비활성화합니다. MipMaps의 원리는 그리기 공간에 있는 그리기 개체의 크기를 기반으로 적절한 텍스처 수준을 선택하는 것입니다. 이렇게 하면 메모리/비디오 메모리 오버헤드가 30% 증가합니다. UI는 일반적으로 카메라 거리에 관계없이 길이와 너비가 동일하므로 UI의 MipMap을 비활성화해야 합니다.

  • UI 텍스처의 원래 크기를 유지합니다. 크기 조정은 일반적으로 추가 오버헤드를 발생시키고 UI를 흐리게 하며 이미지 품질을 저하시킵니다.

  • 큰 배경 이미지를 사용하지 마세요. 큰 배경 이미지는 메모리를 소모하므로 일반적으로 공유할 수 없습니다. 9개의 정사각형 그리드와 세부 도면으로 구성할 수 있습니다.

2.3 글꼴

글꼴이 성능을 저하시킨다고 해도 과언이 아닙니다. 게임에 사용되는 글꼴은 일반적으로 ttf 형식이며 단일 ttf 글꼴 라이브러리의 범위는 510M에서 1020M까지입니다. 텍스트를 그리기 전에 엔진은 글꼴 라이브러리를 메모리에 로드하므로 많은 메모리 공간을 차지합니다. 텍스트가 그려지면 엔진은 글꼴 텍스처를 캐시하기 위해 메모리에 있는 여러 텍스처 아틀라스를 엽니다. 각 단어에는 최소한 두 개의 삼각형이 필요합니다. 그림자/획/광선 등의 효과가 있는 경우 삼각형 수를 여러 번 늘릴 수 있습니다(아래 그림).

imgimg

다음 제안 사항을 사용하여 글꼴 성능을 최적화할 수 있습니다.

  • 글꼴 파일 수를 제어합니다. 시스템 기본 글꼴 외에 1~2개의 사용자 정의 글꼴을 제어하는 ​​것이 적합합니다.

  • 글꼴 그림자/획/글로우 효과를 덜 사용합니다.

  • 글꼴 라이브러리에서 쓸모없는 글리프를 제거합니다. FontSubsetGUI 또는 FontPruner를 사용하여 글꼴 라이브러리를 줄일 수 있습니다.

글꼴 슬리밍에 대한 자세한 내용은 여기를 참조하세요.

2.4 모델

모델, 특히 뼈대 애니메이션이 포함된 모델은 성능 소모에서 차지하는 비중이 매우 크며, CPU/GPU/메모리/비디오 메모리에 대한 부담을 크게 증가시키게 됩니다. 따라서 모델 최적화가 특히 중요합니다. 모델에는 버텍스/인덱스/재료 등을 포함하여 많은 데이터가 포함되며 정점에는 pos/color/uv/normal/tagent/skin과 같은 데이터가 포함될 수 있습니다. 우리는 이 데이터로부터 최적화를 시작할 수 있습니다.

위 그림의 모델 정점에는 pos/color/uv/normal/skin 등의 데이터가 포함되어 있습니다.

2.4.1 모델 데이터

  • 모델의 버텍스 데이터에는 일반적으로 pos/uv가 포함되지만, 색상/노멀/스킨 등의 데이터는 객체 유형에 따라 다르게 처리됩니다. 예를 들어 정적 개체의 경우 스킨을 제거할 수 있습니다. 버텍스 색상을 변경할 필요가 없으면 색상을 제거할 수 있습니다.

  • 모델 내 pos.xyz/uv.xy/color.rgba 등의 데이터는 기본적으로 32비트 부동 소수점 숫자로 저장됩니다. 데이터 표현 범위는 대부분의 게임의 애플리케이션 시나리오를 훨씬 능가하며 16비트 부동 소수점 숫자로 압축할 수 있습니다.

  • 모델의 인덱스 데이터에는 삼각형이 참조하는 버텍스 번호가 저장됩니다. 기본적으로 32비트 unsigned int에 저장됩니다. 그러나 대부분의 모델은 16비트 unsigned short의 범위를 초과할 수 없으므로 16비트 정수이면 충분합니다.

  • Unity 엔진은 모델 가져오기 패널에서 최적화 매개변수를 설정할 수 있습니다(아래 그림).

2.4.2 모델 보조 도구

아티스트가 제작하는 모델은 일반적으로 고정밀 모델입니다. 효과는 좋지만 중저가형 기계에서는 이렇게 높은 정밀도가 요구되지 않는 경우가 많습니다. 이때 최적화를 위해 일부 도구를 사용해야 합니다. 다음은 Unity의 도구를 주로 소개합니다. 다른 엔진에도 비슷한 도구가 있어야 합니다.

  • MeshBaker: 여러 모델을 하나의 모델로 결합하여 모델 수와 Draw Call 수를 줄일 수 있는 모델 병합 플러그인입니다. 주로 장면 및 지상 정적 개체와 같은 정적 개체를 병합하는 데 사용됩니다.

  • SimpleLOD: 오프라인 또는 런타임 중에 모델에 대한 표면 축소 최적화를 수행할 수 있고 일괄 처리 도구로 쉽게 만들 수도 있는 모델 표면 축소 라이브러리입니다.

2.4.3 모델 아트 사양

모델에 하나의 재료만 사용하십시오. 머티리얼에 사용되는 텍스처의 크기는 적당해야 합니다. 너무 크면 메모리가 낭비됩니다. 너무 작으면 화질이 흐려집니다. 모델링 시 모델 내부에 보이지 않는 꼭지점과 삼각형 면을 제거하고, 겹치거나 인접한 꼭지점을 병합하고, 모델의 꼭지점과 면 수를 줄입니다. 예술로 제작된 모델의 정확도가 너무 높아지는 것을 방지하기 위해서는 모델의 꼭지점과 면의 수를 제한할 필요가 있습니다. 모델에 골격 애니메이션이 있는 경우 골격 수를 제한해야 합니다. 단일 모델의 뼈대 수를 70개 이하로 제한하는 것이 가장 좋습니다. 그렇지 않으면 많은 저가형 장치가 GPU 스키닝을 지원할 수 없습니다. 모델을 분류하기 위해 중요한 모델에는 더 많은 뼈가 있을 수 있고, 덜 중요하거나 중요하지 않은 모델에는 더 적은 뼈가 있을 수 있습니다. 다양한 모델에 대한 Game Z의 제한 사항은 다음과 같습니다.

2.5 시나리오

  • 지형이 복잡하지 않은 경우(예: Honor of Kings/LOL의 전투 장면) 지형을 사용하지 말고 간단한 모델로 교체하세요. 표면의 디테일은 텍스처로 표현될 수 있습니다. 표면 텍스처는 적절한 크기여야 하며 일반적으로 1024x1024를 넘지 않아야 합니다.

  • 지형 메시와 지상의 정적 개체에서 그림자를 제거합니다. 일부 개체에 그림자가 정말로 필요한 경우 표면 질감을 만들 때 아티스트에게 그림자를 추가하도록 요청할 수 있습니다.

  • 지형에는 보이지 않는 곳이 많으며, 거기에 있는 모델 메시를 삭제할 수 있습니다.

  • 일반적으로 지상에는 많은 장식과 특수 효과가 있습니다. 표면 수 및 기타 사양이 한도를 초과하는지주의하십시오.

  • 한 장면에 하나의 방향성 조명만 사용하고 실시간 픽셀 조명은 하나만 사용하세요. 실시간 조명 대신 조명 맵을 사용하세요. 조명 맵 텍스처 크기는 너무 커서는 안 됩니다.

  • 장면의 얼굴과 개체 수를 제한합니다. 일괄 병합 도구를 사용하여 유사한 지표면과 오프라인으로 정적 개체를 병합하여 장면 복잡성을 줄입니다.

  • 낮은 이미지 품질에서 가장 낮은 모듈러스 장면 사용, 장면 특수 효과 숨기기 등 장면에 대한 이미지 품질 그레이딩 전략을 사용합니다.

  • 표면에 탐색 메시가 있는 경우 탐색 메시는 보다 간소화된 모델을 사용할 수 있으며 복잡한 가장자리를 간단한 기하학적 다각형으로 단순화할 수 있습니다.

위 그림은 게임 Z의 한 장면을 보여줍니다. 표면 객체는 상대적으로 복잡하지만 지형 그리드(녹색 선으로 표시)는 매우 간단하며, 복잡한 표면 구조를 표현하기 위해 적은 수의 삼각형을 사용합니다.

2.6 입자

입자 효과는 성능을 저하시키는 큰 요인이기도 하며 주로 다음과 같은 경우에 반영됩니다.

  • 프레임당 계산량이 많습니다. 이미터/이펙터/곡선 보간 등을 포함하여 CPU 성능을 소모합니다.

  • 빈번한 메모리 작업. 파티클의 라이프사이클 동안 인스턴스는 지속적으로 생성/삭제됩니다. 캐싱 메커니즘을 사용하더라도 빈번한 읽기와 메모리 조각화는 피할 수 없습니다.

  • 매 프레임마다 GPU에 데이터를 업데이트합니다. 버텍스 버퍼를 잠그는 것부터 GPU에 데이터를 쓰는 것까지, 스레드 대기가 발생하고 CPU에서 GPU 대역폭에 대한 부담이 증가합니다.

  • 다수의 드로우 콜을 추가했습니다. 입자 효과는 일반적으로 다양하고 많은 재료를 사용하므로 엔진이 이를 일괄적으로 최적화할 수 없고 도면 수가 크게 늘어납니다.

  • 오버드로가 발생합니다. 파티클은 일반적으로 Alpha Blend를 활성화하므로 동일한 화면에 파티클이 많이 있을 때 심각한 오버드로가 발생할 수 있습니다. (아래 사진)

게임 Z 메인 인터페이스의 오버드로잉 상황. 흰색일수록 오버드로잉이 심각한 것입니다. 입자를 사용한 경우 색상이 흰색으로 나타나는 경향이 있음을 알 수 있습니다.

입자는 심각한 성능 저하를 유발하므로 먼저 리소스를 최적화하고 표준화해야 합니다. 구체적인 방법은 다음과 같습니다.

  • 입자 특성을 최적화합니다. 그림자와 조명을 끄십시오. 가능하다면 텍스처의 알파 채널을 제거하고 알파 혼합 및 알파 테스트를 끄십시오.

  • 입자에 대한 고급 특수 효과를 비활성화합니다. 모델 입자/모델 방출기/입자 충돌기 등

  • 최소한의 입자 효과를 사용하십시오. 불필요한 입자 효과를 끄고 간단한 효과로 교체하세요.

  • 입자의 재료 수를 제어합니다. 특수 효과에는 일반적으로 여러 입자 시스템이 포함됩니다. 그들은 내장된 머티리얼을 사용하고 동일한 머티리얼 인스턴스를 사용하려고 합니다.

  • 입자 크기와 질감 크기를 제어합니다. 입자 크기를 조정하고, 오버드로잉 속도를 늦추고, 텍스처 크기를 제어하여 대역폭을 줄이고 렌더링 성능을 향상시킬 수 있습니다.

  • 입자 특수 효과 아트 사양을 개발합니다. 아래는 Game Z의 파티클 사양입니다.

게임 Z 파티클 아트 사양

  • 단일 입자의 방출 개수는 50개를 초과하지 않습니다.

  • 입자의 크기를 줄입니다. 면적이 클수록 성능이 더 많이 소모됩니다.

  • 입자 맵은 2의 N승이어야 합니다. 128x128이나 256x256 등 아주 적은 양으로 64x64 내에서 조절해 보시고, 최대치는 256x256을 넘지 않도록 하세요.

  • 파티클맵의 알파채널을 최대한 제거합니다.

  • 알파테스트를 사용하지 마세요.

  • 병합 렌더링의 최적화 확률을 높이기 위해 기존 재료를 사용해 보십시오.

  • 모바일 디렉토리의 자료가 선호됩니다.

  • 입자를 만들기 위해 모델을 사용하지 마십시오. 사용하는 경우 모델 면 수를 100개 이내로 제어하고 최대 입자 수를 5개 이내로 제어합니다.

  • 단일 특수 효과 렌더링 데이터 제한:

작은 특수 효과(히트 효과, 버프 효과 등)의 면 및 버텍스 수는 80 이내, 텍스처는 64*64 이내, 재질 수는 2 이내여야 합니다.

  • 중형 특수효과(스킬 특수효과 등)의 면 및 꼭지점 수는 150 이내, 텍스쳐는 128*128 이내, 재질 수는 4 이내로 하세요.

  • 대규모 특수효과(글로벌 특수효과, 대형 불덩어리 등)를 위한 면 및 정점의 개수는 300개 이내, 텍스처는 256*256 이내, 재질의 개수는 6개 이내로 하여야 합니다.

2.7 재료

재료를 잘 제어하지 않으면 엔진의 배치 최적화가 파괴되고 렌더링 소비가 증가합니다. 따라서 프로젝트 초기에는 자재의 관리와 표준화가 필요하다.

  • 엔진에 내장된 소재를 최대한 활용하세요. 엔진에 내장된 재료는 기본 재료 요구 사항을 충족할 수 있으며 일반적으로 최적화되어 있으므로 내장 재료가 선호됩니다. 모바일 게임인 경우 일반적으로 엔진에는 모바일 버전의 재료 라이브러리도 있습니다.

  • 공유 사용자 정의 재료 라이브러리를 만듭니다. 엔진에 내장된 재질이 프로젝트 요구 사항을 충족하지 못하는 경우 문제를 해결하려면 사용자 정의 재질을 추가해야 합니다. 맞춤형 자재의 경우 규칙에 따라 적절하게 관리하고 분류하고 이름을 지정하는 것이 필요합니다. 이는 자료 공유 및 관리에 큰 이점이 됩니다.

  • 새로 추가된 자료가 기존 자료와 중복되는지 정기적으로 확인하세요. 유사한 자료가 있으면 중복된 자료를 삭제하세요. 이 작업은 메인 프로그램이나 메인 아티스트가 수행할 수도 있고, 툴을 통해 확인할 수도 있습니다.

2.8 아트 사양 개발

미술 사양을 공식화할 때 수석 아티스트/최고 디자이너/프로듀서와 협상하고 프로젝트의 특정 조건을 결합하고 합리적인 미술 사양 매개변수를 제공하고 이를 문서에 작성해야 합니다. 사양이 설정된 후에는 프로젝트의 모든 아트 사양이 사양에 맞는지 정기적으로 확인하고, 호환되지 않는 리소스를 찾아 아트가 이를 수정하도록 해야 합니다. 아트가 규정을 준수하는지 확인하려면 일괄 처리 도구를 작성하여 효율성을 높일 수 있습니다.

특수 효과/장면/캐릭터 리소스를 높음, 중간, 낮음 구성 버전으로 일괄 처리할 수 없는 경우 아티스트는 각 이미지 품질 수준에 대해 서로 다른 리소스를 제작하는 것이 좋습니다.

3. CPU 최적화

성능 최적화에서 가장 중요한 부분은 CPU입니다. CPU 성능이 최적화되면 목표 달성 성공률의 절반이 달성됩니다.

3.1 캐싱 계산 결과

캐시 계산은 시간에 대한 공간의 고전적인 적용입니다. CPU 계산을 많이 소모하고 프레임마다 계산 결과를 변경할 필요가 없는 로직에 적합합니다. 의사코드 구현:

cpp
std::map<KeyType, ValueType> _cache;

ValueType Calculate(KeyType key)
{
    // 로부터 캐싱(Cache) 내에서 페치/가져오기(Fetch)결과 ,반환(Return)。
    if (_cache.count(key) > 0)
    {
        return _cache[key];
    }
    // 캐싱(Cache),실행(Execute)의 계산/산출(Calculate),캐싱(Cache)계산/산출(Calculate)결과 。
    ValueType res = DoCalculation(key);
    _cache[key] = res;
    return res;
}

적용 가능한 시나리오의 예:

  • 복잡한 수학적 계산. Sin/Cos/Pow/Sqrt와 같은 연산에는 일정량의 계산이 필요합니다. 첫 번째 계산인 경우 결과를 캐시할 수 있습니다. 다음에 동일한 계산이 발생하면 캐시에서 직접 값을 가져올 수 있습니다.

  • 물리 시뮬레이션 결과. 객체의 물리적 시뮬레이션 과정은 많은 계산을 소모하지만 일부 객체는 시뮬레이션 후 정적 상태에 있습니다. 이전 시뮬레이션 결과를 저장하여 계산이 프레임마다 업데이트되는 것을 방지할 수 있습니다. 최신 주류 상용 엔진은 이러한 최적화를 지원합니다.

  • 라이트맵. 라이트 맵은 장면의 정적 라이트와 그림자를 계산하여 오프라인 맵으로 캐시합니다. 렌더링할 때 라이트 맵의 색상만 샘플링하면 되므로 조명 계산의 복잡성이 크게 줄어듭니다.

  • 검색결과. 예를 들어 장면 노드 검색, 장면 노드는 일반적으로 트리 구조를 채택합니다. 검색 중인 노드의 깊이가 매우 깊다면 순회 횟수가 크게 늘어납니다. 이 경우 검색 결과를 캐시해야 합니다.

  • 복잡한 계산을 위한 논리 모듈. 복잡한 계산이 포함된 게임 논리 모듈의 경우 캐싱을 사용하여 CPU 부하를 줄일 수 있습니다.

3.2 전처리

캐싱 방법은 공간을 시간으로 교환한다는 아이디어를 사용하므로 메모리 오버헤드가 증가합니다. 전처리는 시간을 전송하는 아이디어이므로 메모리 소비를 늘리지 않습니다. 로드나 계산에 많은 시간이 걸리는 로직은 프로그램 시작 후/장면 로드 시/인터페이스 전환 전/전투 시작 전 미리 계산하거나 로드하여 렌더링 중 과도한 CPU 부하로 인한 프레임 속도 변동이나 지연을 방지합니다.

3.3 프레임 제한 방법

프레임 제한 방법은 간단하고 투박하지만 그 효과는 상당합니다. 일반적인 최적화 방법입니다. 빈도를 제한하는 개체로는 World.Update, 물리적 시뮬레이션, 입자 계산, 캐릭터 AI 처리, 캐릭터 상태 업데이트 등이 있습니다. 프레임 제한은 다음 방법을 통해 달성할 수 있습니다.

  • 계산 방법. 변수를 이용하여 업데이트 횟수를 기록하고, 특정 횟수가 누적될 때마다 업데이트를 수행합니다.
cpp
int _frameCounter = 0
void Update(float time)
{
    _frameCounter += 1;
  // 갱신(Update)주파수(Frequency)로 10회 。
  if (_frameCounter % 10 == 0) 
  {
    DoUpdate();      // 실행(Execute)의 갱신(Update)
    _frameCounter = 0;  // 리셋/초기화(Reset)
  }
}
  • 타이머. 타이머 메커니즘을 사용하여 고정된 시간마다 업데이트를 트리거합니다.

  • 코루틴. 코루틴은 메인 스레드에서 실행되는 의사 스레드이지만 멀티스레딩의 부작용 없이 비동기 작업을 시뮬레이션할 수 있습니다. 따라서 프레임 제한 작업에도 사용할 수 있습니다.

  • 이벤트 트리거. 각 프레임의 쿼리 상태를 이벤트 트리거링으로 변경하는 것도 게임에서 흔히 사용되는 최적화 방법으로, 프레임을 제한하는 데에도 매우 효과적입니다.

3.4 1차 및 2차 방법

1차 및 2차 방법은 LOD 기술과 동일한 효과를 갖습니다. 아이디어는 중요도에 따라 개체를 높음, 중간, 낮음 수준으로 나눈 다음 다양한 수준에서 다양한 복잡성의 효과나 계산을 사용하는 것입니다. 이 아이디어는 게임에서 널리 사용될 수 있으며, 기본적으로 모든 고비용 로직이나 모듈이 이 기술을 사용할 수 있습니다. 예를 들면:

  • 이미지 품질 수준. 게임을 여러 레벨로 나누고, 가장 높은 이미지 품질에서 가장 낮은 이미지 품질까지 다양한 렌더링 기술이나 리소스를 사용하고 다르게 처리합니다.

  • 자원 수준. 장면/특수 효과/조명/물리 효과/내비게이션 및 기타 모듈은 이미지 품질 수준이나 개체 수준에 따라 서로 다른 수준의 리소스에 대응할 수 있으므로 이미지 품질 효과를 고려하면서 전반적인 소비를 줄일 수 있습니다.

  • 문자 분류. 주인공 영웅/보스 등은 소형 몬스터/NPC와 구별됩니다. 전자는 더 자주 업데이트되고 AI 동작은 더 복잡하고 지능적인 반면, 후자는 간단한 효과를 사용하거나 빈도가 줄어든 업데이트를 사용합니다.

3.5 멀티스레딩

많은 분들이 위의 재미있는 애니메이션 사진을 보셨으리라 믿습니다. 오늘날 기기에서는 멀티코어가 대세인데, 메인 코어가 대부분의 작업을 수행하고 너무 바빠서 다른 코어들이 어려움을 겪는 동안 "피를 토한다"는 당혹스러운 상황을 생생하게 보여줍니다. 게임 개발에서는 위에서 언급한 현상을 완화하기 위해 스레드를 생성하여 일부 로직을 분리하고 처리를 위해 다른 CPU 코어에 넘겨줄 수 있습니다. 독립적으로 스레드로 처리할 수 있는 모듈:

  • 파일 IO. 파일 큐에 데이터가 있는지 정기적으로 확인하기 위해 IO 스레드를 설정하십시오. 그렇다면 파일 큐가 빌 때까지 IO 로딩을 시작하고 유휴 폴링 상태로 돌아갑니다. 메인 스레드는 파일 IO를 기다리는 상태가 아닙니다.

  • 뼈대 애니메이션. 같은 화면에 애니메이션 캐릭터가 많으면 CPU 계산을 많이 차지하게 됩니다. 렌더링 프로세스 속도를 높이기 위해 골격 애니메이션 계산을 처리하기 위해 하나 이상의 스레드를 만들 수 있습니다. 그러나 동기화 문제가 발생합니다.

  • 입자 계산. 입자의 멀티스레딩은 골격 애니메이션과 유사하며 동기화 문제도 있습니다.

  • 렌더링 스레드. 렌더링을 스레드로 분리하면 주로 CPU와 GPU가 서로 기다리는 문제가 해결됩니다. 일반적인 접근 방식은 렌더링 스레드를 설정하고, 주기적으로 렌더링 대기열을 쿼리하여 데이터가 있는지 확인하고, 데이터가 있으면 그리기를 위해 GPU에 제출하는 것입니다.

  • 오디오 및 비디오 코덱. 게임에 오디오 및 비디오 재생이 포함되어 있고 소비량이 많은 경우 오디오 및 비디오 코덱을 처리하기 위해 별도의 스레드를 열어야 합니다.

  • 암호화 및 암호 해독. 암호화 및 복호화에 관련된 알고리즘은 일반적으로 더 복잡하고 더 많은 CPU 성능을 차지하며 파일 IO 또는 네트워크 IO가 수반되는 경우가 많으므로 이때 처리를 위해 독립 스레드에 넘겨주는 것이 매우 필요합니다.

  • 네트워크 IO. 현재 대부분의 게임은 네트워크 처리가 메인 스레드에 영향을 미치지 않도록 네트워크 데이터를 보내고 받기 위해 특별히 스레드를 엽니다.

스레드 전환으로 인해 추가 오버헤드가 발생하고 동기화 및 교착 상태 문제도 프로그래머의 머리 위에 걸려 있는 큰 칼인 논리 복잡성과 디버깅 어려움을 증가시킨다는 점은 주목할 가치가 있습니다.

3.6 엔진 모듈

엔진 내에서 CPU 성능을 가장 많이 소모하는 모듈에는 일반적으로 골격 애니메이션/입자 계산/물리적 시뮬레이션/내비게이션 메시/카메라 자르기 및 렌더링 등이 포함됩니다.

3.6.1 애니메이션

  • 애니메이션 빈도를 줄입니다.

  • 키프레임 데이터를 줄입니다.

  • 애니메이션 보간 결과 캐싱.

  • 복잡한 보간(베지어 곡선)을 단순 곡선 보간(선형 보간)으로 대체합니다.

  • 애니메이션에 첨부된 객체의 가시성을 결정합니다. 표시되지 않으면 애니메이션이 업데이트되지 않습니다.

  • 동일한 화면에 표시되는 애니메이션 수를 제어합니다.

  • 이미지 품질에 따라 다양한 수준의 애니메이션 리소스가 로드됩니다.

3.6.2 물리학

  • 정적 물리학은 정적 충돌체만 사용합니다.

  • 물리 시뮬레이션 빈도를 줄입니다.

  • 복잡한 충돌기(예: 모델 충돌기)를 비활성화하고 대신 여러 개의 간단한 기하학적 충돌기의 조합을 사용하십시오.

  • 객체가 보이지 않으면 물리 시뮬레이션을 끄십시오.

  • 동일한 화면에 있는 물리적 개체의 수를 제어합니다.

  • 객체의 중요도에 등급을 매기거나, 단순 물리 효과를 사용하거나, 중요도가 낮은 캐릭터에 물리 효과를 삭제합니다.

  • Raycast Ray Inspection은 사용하기 쉽지만 성능 소모도 높습니다. 반복 통화를 방지하려면 영상 주파수를 최대한 줄여야 합니다.

3.6.3 입자

  • 입자 자원 최적화: 자세한 내용은 2.6을 참조하세요.

  • 일부 입자 효과를 순차 프레임 렌더링으로 변환하는 것을 고려해보세요.

  • 캐싱/사전 로딩/사전 계산 최적화가 가능한지 고려하세요.

  • 특수효과의 중요도에 등급을 매기면 중요하지 않은 파티클은 계산되거나 축소되지 않습니다.

  • GPU의 부담이 CPU의 부담보다 작을 경우 파티클 업데이트 계산이 GPU 측으로 옮겨질 수 있습니다.

  • 입자 개체의 가시성을 결정합니다. 표시되지 않으면 계산되거나 렌더링되지 않습니다.

3.6.4 탐색

내비게이션 메시를 단순화하고 최소한의 면을 사용하여 울퉁불퉁한 도로를 평평한 표면으로 바꾸는 등 복잡한 지면의 내비게이션 구조를 표현합니다(자세한 내용은 2.5 참조). 길 찾기 계산은 복잡하며, 길 찾기의 클러스터링을 방지하려면 호출 빈도를 논리적으로 제어해야 합니다.

3.7 논리 최적화

로직 소비는 높은 CPU 부담의 원인이기도 하며 주로 AI/알고리즘/스크립트 및 기타 모듈에 반영됩니다.

3.7.1 AI 최적화

플레이어 조작을 단순화하고 몬스터를 더욱 인간화하기 위해 현재 대부분의 게임에는 AI 기능이 포함되어 있으며, AI 행동 트리(아래 그림)가 AI 구현의 핵심이다.

그러나 일반적인 상황에서는 AI 동작 트리를 프레임마다 업데이트해야 하며, 업데이트하기 전에 적절한 노드를 찾을 때까지 루트 노드에서 검색해야 할 때마다 업데이트해야 합니다. 최악의 경우 전체 트리를 순회하게 됩니다. 일반적으로 사용되는 AI 행동 트리 최적화 방법:

  • 현재 작업 노드 경로를 캐시합니다. 즉, 현재 업데이트 중인 노드와 해당 상위 노드가 캐시됩니다. 다음에 업데이트할 때 전체 트리를 순회하지 않도록 이 경로 검색에 우선순위가 부여됩니다. 현재 작업 노드가 지속적이면 이 기간 동안 노드를 검색할 필요가 없으며 노드를 직접 업데이트하면 됩니다.

  • AI 업데이트 빈도를 줄입니다. 즉, 부담을 줄이기 위해 AI 빈도를 강제로 줄이는 것이다. 또한 AI 업데이트를 최적화하기 위해 기본 및 보조 방법/확산 프레임 방법/동적 프레임 조정 방법을 시도해 볼 수도 있습니다.

3.7.2 알고리즘 최적화

CPU를 가장 많이 소모하는 알고리즘이나 로직을 찾아 최적화하는 것이 아이디어다.

  • 시간을 위한 공간. 사전 정렬/전처리/캐싱/동적 프로그래밍 및 기타 아이디어를 사용하여 CPU 성능을 교환하십시오.

  • 더 빠른 알고리즘을 선택하세요. 이는 데이터 구조 및 알고리즘의 범주에 속합니다. 아이디어는 O(n2)를 O(n) 또는 O(logn)으로 줄이는 것입니다. 자세한 내용은 "알고리즘 입문", "게임 프로그래밍 알고리즘 및 기법", "게임 핵심 알고리즘 프로그래밍 내부자" 등의 도서를 참고하세요.

3.7.3 스크립트 최적화

일반적으로 엔진의 맨 아래 레이어는 C++와 같은 기본 언어로 구현되는 반면, 스크립트는 동적 언어(Java/C#/Lua/Python)로 구현됩니다. 그 사이에는 두꺼운 시뮬레이터 또는 캡슐화 레이어가 있습니다. Unity 엔진과 C#의 관계는 다음과 같습니다.

Unity는 게임을 패키징할 때 Mono를 통해 C# 코드를 IL 중간 언어로 생성합니다. iOS 플랫폼인 경우 IL2CPP를 통해 C++ 코드도 생성됩니다. 쉽게 말하면 Unity 엔진 코어와 C# 등 스크립팅 언어 간의 상호 작용은 Mono의 두꺼운 중간 계층을 통과합니다. 이로 인해 추가 오버헤드가 발생하여 스크립팅 언어가 비효율적으로 실행됩니다. 스크립트 오버헤드를 줄이는 몇 가지 방법은 다음과 같습니다.

  • 스크립트 내에서 빈 콜백을 제거합니다. 스크립트 객체의 콜백 함수가 비어 있어도 엔진 코어 및 스크립트 레이어의 오버헤드가 계속 발생합니다.

  • 스크립트 개체가 다른 개체를 참조하는 경우 초기화 중에 캐시될 수 있습니다.

cpp
class Tester
{
    private Object _obj = null;

    void init()
    {
        _obj = FindObject("MyObjectName"); // 초기화(Initialize)까지 。
    }

    void update()
    {
        if (_obj)
        {
            _obj.update();    // 。
        }
    }
}
  • 프레임 업데이트/루프 문 내에서 힙에 임시 개체를 생성하지 마세요. 임시 개체는 루프 문 외부로 이동하거나 클래스의 멤버로 선언되고 초기화 중에 값을 할당할 수 있습니다.

  • 가시성 콜백을 활용하세요. 가시성 콜백 내에서 비활성화/복원 작업을 수행하는 것은 시간이 많이 걸리는 작업입니다.

  • 문자열 연결로 인해 임시 개체가 발생하기 쉬우므로 주의하세요. C#의 StringBuilder와 같은 보다 효율적인 접합 방법을 사용할 수 있습니다.

3.7.4 조건부 테스트

조건부 테스트는 주로 시간이 많이 걸리는 통화 최적화에 사용됩니다. 각 프레임에서 업데이트해야 하는 작업을 다양한 조건 확인에 추가하여 시간이 많이 걸리는 작업의 확률을 줄입니다.

cpp
void Update()
{
    DoCalculation();    //
}
// :
void Update()
{
    // 추가(Add)조건
    if (_dirty && _visible && _moved && _timeInterval>0.1)
    {
        DoCalculation();
    }
}

3.7.5 중복 방지

게임에는 일반적으로 많은 모듈, 복잡한 캐릭터 상태 머신, 많고 복잡한 트리거 이벤트가 포함됩니다. 시간이 많이 걸리는 동일한 API가 동일한 프레임에서 여러 번 호출되는 경우가 많아 추가 오버헤드가 발생합니다. 이 문제는 조건 테스트, 시간 간격, 로그 출력, 호출 스택 디버깅 등을 통해 해결될 수 있습니다.

4. 렌더링 최적화

렌더링 최적화의 목적은 그리기 호출을 줄이고, 렌더링 상태 전환 오버헤드를 줄이고, 비디오 메모리 사용량을 줄이고, 대역폭과 GPU 부담을 줄이는 것입니다. 렌더링 최적화를 설명하기 전에 먼저 렌더링 성능 소비 지점을 이해해 보겠습니다.

  • 드로 콜 수

일부 엔진에서는 Draw Call을 SetPass Call이라고도 합니다. Draw Call은 게임이 OpenGL/D3D와 같은 그래픽 렌더링의 드로잉 API(예: OpenGL의 glDrawArray 및 glDrawElements)를 한 번 호출하는 경우입니다. 하나의 그리기 호출은 많은 데이터/상태/계산을 포함하는 전체 렌더링 파이프라인(아래 그림)을 통해 완전히 실행됩니다. 그리기 전에 다양한 GPU 데이터가 생성되며 이러한 데이터는 매 프레임마다 업데이트될 수 있습니다. 데이터 업데이트에는 대역폭이 포함됩니다.

따라서 프레임당 드로우 콜 수는 렌더링 성능의 주요 지표입니다.

  • 렌더링 상태 스위치

각 Draw Call 전에 깊이 테스트를 켤지 여부/알파 테스트를 켤지 여부/알파 블렌드를 켤지 여부 등과 같은 일련의 렌더링 상태가 그래픽 렌더링 레이어에 대해 설정됩니다. 이러한 상태는 궁극적으로 그래픽 렌더링 드라이버 레이어(아래 그림)를 통해 GPU에 적용됩니다.

위 그림에서 볼 수 있듯이 애플리케이션(게임)에서 보낸 렌더링 명령은 OpenGL/DirectX 등의 그래픽 레이어와 그래픽 카드 드라이버 레이어를 거쳐 최종적으로 GPU 하드웨어에 적용됩니다. 최신 그래픽 카드 드라이버는 상태 관리/내결함성 처리/논리 계산/비디오 메모리 관리 등 많은 작업을 수행하기 때문에 캡슐화되어 더 많은 성능을 소비합니다. 따라서 상태 전환을 최대한 줄이는 것이 렌더링 성능을 최적화하는 중요한 방법입니다.

  • 대역폭 로드

여기서 대역폭은 마더보드 버스를 통해 GPU에 데이터를 전송하는 CPU의 능력을 의미하며 단위는 일반적으로 GB/s입니다. 물론 GPU도 버스를 통해 CPU로 데이터를 전송할 수 있지만, CPU에서 GPU로의 전송 능력은 훨씬 떨어진다.

광대역에는 GPU와 비디오 메모리 간, 그리고 GPU의 다양한 구성 요소 간 데이터 전송도 포함됩니다.

위 그림과 같이 CPU와 GPU는 PCI-e 버스를 통해 연결됩니다. 그들 사이에는 전송 용량의 상한이 있습니다. 이 상한은 대역폭입니다. 그리기를 위해 전송해야 하는 데이터가 대역폭보다 큰 경우(즉, 대역폭 부하가 너무 높은 경우) 화면 정지, 프레임 건너뛰기, 찢어짐, 지연, 검은색 화면 등 다양한 이상이 발생합니다.

  • 비디오 메모리 사용량

비디오 메모리는 GPU 내부에 통합된 전용 메모리인 그래픽 카드의 메모리입니다. 일반적으로 버텍스/인덱스/텍스처/다양한 버퍼와 같은 데이터를 저장하는 데 사용됩니다. 게임의 비디오 메모리 사용량이 너무 높으면 비디오 메모리 할당이 실패하여 화면이 이상하거나 프로그램이 충돌할 수도 있습니다.

  • GPU 계산량

최신 그래픽 카드는 기본적으로 Vertex Shader/Geometry Shader/Fragment Shader(아래 그림) 및 래스터라이제이션/조각 작업과 관련된 프로그래밍 가능한 렌더링 파이프라인을 지원합니다. 따라서 셰이더가 너무 복잡하거나 조각이 너무 많으면 GPU 계산량이 크게 늘어나고 렌더링 성능이 저하됩니다.

구체적인 렌더링 비용을 이해한 후 다음 방법을 사용하여 최적화할 수 있습니다.

4.1 배치

Batch는 Draw Call을 한 번만 호출할 수 있도록 여러 모델을 하나의 모델로 결합하는 최적화 방법입니다. 결합된 일괄 처리는 Draw Call 수 문제를 해결합니다. 일괄 처리의 조건은 병합되는 모든 모델이 동일한 재료를 참조하는 것입니다. 그렇지 않으면 일괄 처리를 정상적으로 수행할 수 없습니다.

4.1.1 오프라인 배치

오프라인 배칭이란 게임을 실행하기 전에 도구를 사용하여 관련 리소스를 일괄 처리하여 엔진의 실시간 배칭 부담을 줄이는 것을 의미합니다. 정적 모델과 장면 개체는 오프라인 일괄 처리에 적합합니다. 장면 표면 장식: 돌/벽돌 등. 오프라인 일괄 처리 방법에는 다음이 포함됩니다.

  • 예술은 전문 모델링 도구를 사용하여 일괄 처리됩니다. 3D Max/Maya 등과 같은

  • 엔진 플러그인이나 도구를 활용하세요. 예를 들어 Unity의 플러그인 MeshBaker 및 DrawCallMinimizer는 정적 개체를 일괄 처리할 수 있습니다.

  • 자체 제작한 오프라인 일괄 처리 도구입니다. 타사 플러그인이 프로젝트 요구 사항을 충족할 수 없는 경우 프로그램은 특별히 오프라인 일괄 통합 도구를 구현해야 합니다.

4.1.2 런타임 배치

오프라인 일괄 처리와 달리 실시간 일괄 처리는 게임이 실행되는 동안 게임 엔진에 의해 완료됩니다. Unity 엔진은 정적 배칭과 동적 배칭으로 구분됩니다.

  • 정적 배치

정적 일괄 처리 자격을 얻으려면 두 가지 조건이 있습니다. 첫째, 모델에 정적 마크가 있습니다(즉, 개체가 정적이며 움직임/애니메이션/물리적 등을 가질 수 없음). 둘째, 동일한 머티리얼 인스턴스가 참조됩니다. 정적 배칭의 확률을 높이려면 장면 객체를 가능한 정적으로 설정하고 유사한 객체는 동일한 재질을 참조합니다.

  • 동적 배치

동적 일괄 처리는 이동할 수 있지만 Unity 요구 사항과 같이 더 엄격한 요구 사항이 있는 모델을 위한 것입니다.

모델에 300개 미만의 정점과 900개 미만의 버텍스 어트리뷰트이 있습니다.

  • 미러 변환은 있을 수 없습니다.

  • 동일한 머티리얼 인스턴스를 사용합니다(참고: 이는 인스턴스이므로 동일한 머티리얼과 다른 인스턴스는 작동하지 않습니다).

  • 동일한 라이트맵을 사용하세요.

  • 다중 패스 셰이더를 사용할 수 없습니다.

4.1.3 일괄 처리의 부작용

배치 최적화는 그리기 호출을 줄일 수 있지만 부작용도 있습니다.

  • CPU 소모가 증가했습니다. 여러 모델을 하나로 결합하려면 CPU 계산이 필요하며 재료 분류 및 수집과 같은 작업도 포함됩니다.

  • 메모리를 늘립니다. 합성된 모델을 저장하려면 추가 메모리가 필요합니다.

  • 합성할 수 있는 모델 버텍스 수에는 제한이 있습니다. 모바일 게임은 일반적으로 16비트 인덱스를 사용합니다. 합성된 모델이 16비트 부호 없는 정수 범위를 초과하는 경우 렌더링 예외가 발생합니다.

4.2 렌더링 상태 최적화

4.2.1 상태 캐시

엔진 측에서는 상태 캐싱을 사용하여 렌더링 파이프라인 전환을 줄일 수 있습니다. 의사코드:

cpp
class RenderStateCache
{
public:
    void InitRenderStates();
    {
        for (RenderStateType t=RenderStateType.begin; t<RenderStateType.end; ++i)
        {
            _renderStateCache[t] = GetRenderStateFromDevice(t);
        }
    }

    void SetRenderState(RenderStateType state, RenderStateValue value)
    {
        // 만약 설정(Set)의 상태 및 현재(Current)캐싱(Cache)의 ,이면 무시/스킵(Ignore)。
        if (_renderStateCache.count(state) > 0 && _renderStateCache[state] == value)
        {
            return;
        }
        _renderStateCache[state] = value;
        SetRenderStateToDevice(state, value);
    }

private:
    std::map<RenderStateType, RenderStateValue> _renderStateCache;
};

4.2.2 렌더링 상태 권장사항

  • 알파 블렌드를 아껴서 사용하세요. Alpha Blend가 켜져 있으면 일반적으로 깊이 테스트가 꺼집니다. 깊이 테스트는 과도한 조각을 제거하는 데 사용할 수 없으므로 조각 수가 증가하고 초과 그리기가 발생합니다. 그러니 가능한 한 적게 사용하세요.

  • 알파 테스트를 비활성화합니다. 일부 최신 모바일 GPU는 특별한 렌더링 최적화 방법을 사용합니다. 예를 들어 PowerVR은 Tile Based Deferred Rendering(아래 그림 오른쪽)을 사용하며 Alpha Test는 Alpha Blend로 대체될 수 있는 Early-Z 최적화 기술을 파괴합니다. 여기에서 자세한 내용을 확인하세요.

  • 뒤로 자르기를 활성화합니다. 뒷면 자르기를 사용하면 카메라 반대쪽을 향하는 패치를 제거하여 버텍스 및 조각 데이터의 양을 줄일 수 있습니다.

  • MipMap을 켜세요. 켜면 렌더링 시 화면 크기에 따라 적절한 크기의 텍스처가 자동으로 선택되므로 대역폭이 줄어들고 앨리어싱이 줄어들며 이미지 품질이 향상됩니다. 그러나 UI 인터페이스를 열 수 없습니다. 그 이유는 2.2.3을 참고하세요.

  • 안개를 꺼주세요. 고정 파이프라인에만 적용 가능합니다.

  • 앤티앨리어싱을 덜 사용합니다. 그래픽 API에 내장된 앤티앨리어싱은 텍스처 샘플 수를 몇 배로 늘리는 경우가 많으므로(아래 이미지) 주의해서 사용하세요.

4.3 그리기 순서 제어

모델 그리기 순서를 제어하는 목적은 깊이 테스트를 최대한 활용하고 조각에 대한 후속 작업을 줄이는 것입니다. 특히 Early-Z 기술이 도입되면서 이 방법의 효과는 더욱 뚜렷해졌습니다. 그리기 순서는 다음과 같습니다. 먼저 정렬된 불투명 개체를 그린 다음 알파 테스트된 개체를 그리고 마지막으로 투명 개체를 렌더링합니다. 의사코드:

cpp
void Render()
{
    // 1. 드로우/렌더(Draw)반투명(Translucent)
    SortOpaqueObjectsInViewSpace(); // 반투명(Translucent)정렬 ,,의 。
    DrawOpaqueObjects();            // 드로우/렌더(Draw)반투명(Translucent),의 드로우/렌더(Draw)。

    // 2. 드로우/렌더(Draw)Alpha
    SortAlphaTestedObjectsInViewSpace(); // Alpha정렬 ,,의 。
    DrawAlphaTestedObjects();          // 드로우/렌더(Draw)Alpha,의 드로우/렌더(Draw)。

    // 3. 드로우/렌더(Draw)반투명(Translucent)(※ 주의: :드로우/렌더(Draw)반투명(Translucent))
    SortTransparentObjectsInViewSpace(); // 반투명(Translucent)정렬 ,,의 。
    DrawTransparentObjects();            // 드로우/렌더(Draw)반투명(Translucent),의 드로우/렌더(Draw)。
}

4.4 멀티스레드 렌더링

단일 스레드 렌더링 아키텍처에서 CPU 성능 소비가 너무 높으면 GPU의 렌더링 프레임 속도에 영향을 미칩니다. 반대로 GPU 렌더링이 너무 느리면 CPU가 대기 상태로 유지됩니다. 멀티스레드 렌더링은 CPU와 GPU가 서로 기다리는 문제를 해결하는 것입니다. Metal/Vulkan과 같은 아키텍처의 출현을 경계로 두 단계로 나누어진다.

4.4.1 소프트웨어 수준 멀티스레드 렌더링

초기 그래픽 API 및 하드웨어 아키텍처는 멀티스레드 렌더링을 지원하지 않습니다. 이 단계에서 멀티스레드 렌더링이 수행할 수 있는 최적화는 상대적으로 제한됩니다. 렌더링 제출은 논리적 스레드를 차단하지 않도록 스레드로만 분리될 수 있습니다. 오픈 소스 그래픽 렌더링 엔진 OGRE의 멀티스레드 렌더링을 구현하는 방법에는 두 가지가 있습니다.

  • 중간 수준 멀티스레드

각 렌더링 개체에는 두 개의 인스턴스가 있으며 기본 스레드는 데이터 중 하나를 변경하여 다음 프레임의 렌더링 스레드에 사용합니다. (아래 사진)

​ 이 방법은 구현하기가 매우 복잡합니다. 개체의 두 인스턴스를 유지 관리해야 합니다. 멀티코어 CPU에서 확장하는 것은 쉽지 않으며 멀티코어 CPU의 장점을 최대한 활용할 수 없습니다.

  • 낮은 수준의 멀티스레드

이 구현 방법은 렌더링 객체의 버텍스 및 기타 데이터를 복사하고 논리 스레드가 데이터 중 하나를 수정하고 다음 프레임은 렌더링 스레드에서 사용되는 것입니다. 위의 두 가지 솔루션 외에도 논리적 스레드의 특정 논리(예: 업데이트/입자/애니메이션)에 대해 여러 스레드(아래 그림)를 열어 병렬 계산을 수행하고 전체 처리 시간을 단축할 수 있습니다.

4.4.2 하드웨어 수준 멀티스레드 렌더링

최근에는 Metal/Vulkan 그래픽 아키텍처가 등장하고 마침내 하드웨어 수준의 멀티스레드 렌더링 시대가 도래했습니다. 그들의 특성:

  • 경량 드라이버 레이어.

OpenGL의 API와 드라이버는 많은 논리적 캡슐화를 수행하고 상태 머신을 사용하여 렌더링을 구현합니다(아래 왼쪽 그림). Metal/Vulkan의 차이점은 드라이버 계층에서 소량의 작업만 수행하고 애플리케이션에 GPU 하드웨어에 직접 액세스할 수 있는 인터페이스를 제공한다는 것입니다. 경량 패키지입니다(아래 그림의 오른쪽).

imgimg

API 아키텍처 관점에서 보면 Metal/Vulkan의 성능이 훨씬 좋습니다.

  • 하드웨어 수준의 멀티스레드 렌더링을 지원합니다.

Metal/Vulkan은 병렬 렌더링 지침을 지원하므로 CPU의 각 스레드가 렌더링 지침과 데이터를 독립적으로 제출할 수 있습니다. 아래 그림은 렌더링 방법 중 하나를 보여줍니다. 여러 스레드가 서로 다른 그리기 명령을 생성한 다음 별도의 스레드가 렌더링 명령 대기열을 관리하고 그리기를 위해 GPU에 제출합니다.

그래픽 API는 이미 멀티스레드 렌더링 명령 제출을 지원하고 이전 섹션에서 언급한 여러 솔루션과 결합되어 더욱 강력해지고 렌더링 성능도 질적으로 향상됩니다. 현재 주류 상용 엔진은 이미 Metal/Vulkan을 지원하고 있으며 Unity2018.3은 이미 Metal/Vulkan을 지원하고 있습니다.

Unity는 렌더링 설정 패널에서 멀티스레드 렌더링을 활성화할 수 있습니다.

4.5 조명/조명 모델

4.5.1 평면 셰이딩

조명은 표면 노멀 벡터를 기반으로 계산되어 전체 패치에 적용됩니다. 가장 빠르고 최악의 효과이며 물체의 다각형 특성을 쉽게 노출시킵니다(아래 그림).

4.5.2 고로 셰이딩

버텍스 노멀 벡터를 기준으로 조도를 계산한 후 보간법을 사용하여 전체 표면의 조도를 계산합니다. 효과는 플랫 셰이딩보다 약간 더 좋지만 하이라이트에 결함이 있고 전환이 충분히 자연스럽지 않습니다(아래 그림).

최적화를 위해 Phong Shading과 결합할 수 있습니다. 하이라이트가 약한 경우 Gouraud Shading을 사용하고, 빛이 강한 경우에는 Phong Shading을 사용합니다. 이를 통해 효과와 효율성의 균형을 맞출 수 있습니다.

4.5.3 램버트 셰이딩

물체의 표면은 모든 방향에서 동일한 강도로 빛을 반사합니다. 모든 방향으로 균일하게 산란되는 이러한 현상을 빛의 디퓨즈라고 합니다. 램버트의 법칙: 반사광의 강도는 표면 노멀과 광원 방향 사이의 각도에 비례합니다(아래 그림). 이상적인 확산 모델이지만 Gouraud보다 음영이 더 부드럽습니다.

계산 공식:

c=(cm)max(0,nl)

4.5.4 하프 램버트 셰이딩

램버트 컬러링에는 결점이 있는데, 즉 뒷면이 빛을 덜 받아 검은색 상태가 되는 경우가 많고, 수광면과의 대비가 너무 크다(아래 사진 왼쪽). 그래서 향상된 솔루션 중 하나인 Half Lambert Shading이 탄생했습니다. 렌더링된 사진에서 밝은 부분과 어두운 부분의 관계는 그다지 강하지 않으며 전환이 더 자연스럽습니다(아래 오른쪽 그림).

계산 공식:

c=(cm)(a(nl+b)

그 중 ab는 상수이며 일반적으로 0.5, a+b=1.0로 간주됩니다.

4.5.5 퐁 셰이딩

퐁 셰이딩은 조명을 Emissive/Ambient/Diffuse/Specular의 네 부분으로 나누고, 각 부분은 조명 기여도를 독립적으로 계산합니다. 현재 널리 사용되고 있는 조명모델이다.

하이라이트 계산 공식은 다음과 같습니다.

c_{반사광} = \big(c_{빛} \cdot m_{반사광}\big) \max \big(0, \vec{n} \cdot \vec{r}\big)^{m_{광택}

퐁 컬러링 효과는 다음과 같습니다.

4.5.6 블린-퐁 셰이딩

Phong 모델에는 반사 벡터 r이 필요하고 r 계산에 시간이 많이 걸리기 때문에(아래) Blinn Phong이 생성되었습니다.

r=2(nl)nl

Blinn Phong은 Phong을 개선한 것입니다. 방법은 반사 벡터 r을 버리고 l과 v 사이에 중간 벡터 h를 도입한 다음 nh 사이의 각도를 사용하여 계산하는 것입니다.

imgimg

계산 공식 강조 표시:

c_{\text {반사광 }=\left(c_{\text {빛 } \cdot m_{\text {반사광 }\right) \max (0, \vec{n} \cdot \vec{h})^{m_{\text {광택 &#125;&#125;

렌더링하는 하이라이트 범위는 더 크며(아래 그림), 이는 퐁 셰이딩만큼 현실적이지는 않지만 더 효율적입니다.

4.5.7 조명 모델 선택

성능 비교: Flat > Gouraud > Lambert > Half Lambert > Blinn-Phong > Phong. 그러나 화질 효과는 정반대이므로 각 게임은 특정 요구 사항에 따라 선택해야 합니다. 또한 높음, 중간, 낮은 이미지 품질에 대해 다양한 조명 모델을 사용하는 그레이딩 전략을 사용할 수도 있습니다.

4.6 렌더링 경로

4.6.1 레거시 버텍스 조명

엄밀히 말하면 포워드 렌더링의 일종이기도 하지만 일부 엔진(예: Unity)에서는 이를 별도로 추출합니다. 조명은 정점에서 계산되기 때문에 초기 GPU에서 흔히 사용하는 렌더링 방식인 4.5.2 Gouraud Shading과 효과 및 소비가 유사합니다.

4.6.2 포워드 렌더링

포워드 렌더링은 전통적인 렌더링 방법이며 하드웨어에서 널리 지원됩니다. 렌더링의 개념은 렌더링 파이프라인의 프로세스에 따라 단계별로 렌더링하고, 최종적으로 렌더 타겟(Render Target)에 색상을 그리는 것입니다(아래 그림).

소비량은 물체 및 조명의 수와 관련이 있으며 이는 O(NobjectNlight)의 관계입니다. 조명의 수가 많은 장면에서는 부족한 것 같습니다. 조명 계산 의사코드:

cpp
Color color = Color.black;
for each(light in lights)
{
    for each(object in objectsEffectedByLight)
    {
        color += object.color * light.color;
    }
}

일부 엔진(예: Unity)은 조명 수가 많을 때 일부 최적화를 수행합니다. 모든 조명을 밝기별로 정렬하고, 조명의 가장 밝은 부분을 픽셀별로 계산하고, 조명의 중간 부분을 정점별로 계산하고, 낮은 부분에 대해 구면 조화(SH, Spherical Harmonics) 시뮬레이션을 사용합니다. (아래 그림 참조)

4.6.3 디퍼드 렌더링

디퍼드 렌더링의 핵심은 조명 계산을 연기하고 이를 장면 개체 수에서 분리하는 것입니다. 구체적인 방법은 다음과 같습니다. 먼저 모든 객체를 렌더링하되 조명을 계산하지 않고 객체의 렌더링된 픽셀 데이터(Position/Normal/DiffuseColor 및 기타 매개변수)를 해당 GBuffer에 저장합니다. 그런 다음 이 데이터를 사용하여 후처리를 통해 조명 계산을 수행합니다. (아래 사진)

의사코드 구현:

cpp
// :렌더링(Render)라이팅 의 데이터 ,GBuffer。
for each(object in objects)
{
    RenderObjectDataToGBuffers(object);
}

// :라이팅 픽셀(Pixel)계산/산출(Calculate)。
for each(light in lights)
{
    Color color = Color.black;
    for each(pixel in pixels)
    {
        color += CalculateLightColor(light, pixelDatas);
    }
    WriteColorToFinalRenderTarget(color);
}

가장 시간이 많이 걸리는 조명 계산은 포스트 프로세스 단계로 지연되므로 장면의 객체 수와 분리되고 렌더 대상 크기에만 관련됩니다. 복잡성은 O(NlightWrendertargetHrendertarget)입니다. 디퍼드 렌더링은 저가형 장치에서는 지원되지 않으며 OpenGL ES 3.0 이상, 다중 렌더링 텍스처, 더 많은 비디오 메모리 및 대역폭이 필요합니다.

4.6.4 타일 기반 디퍼드 렌더링(TBDR)

Deferred Shading의 단점에 대응하여 Tile-Based Deferred Rendering이라는 개선 솔루션이 등장했습니다. 이 렌더링 방법은 GPU 그래픽 렌더링 아키텍처에서 널리 사용되었습니다. 구현 아이디어:

  • 렌더링 텍스처를 일반적으로 32x32 크기의 작은 타일(타일)로 나눕니다.

  • 타일의 깊이를 기준으로 경계 상자를 계산합니다.

  • 타일의 Bounding Box와 Light가 교차하는지 확인합니다.

  • 분리된 조명을 버리고 타일에 효과적인 조명 목록을 얻습니다.

  • 모든 타일을 탐색하고 각 타일의 유효 조명 목록의 조명을 계산합니다.

4.6.5 렌더링 경로 요약

각 방법의 장단점은 이전에 설명했으며, 성능 소비 및 플랫폼 요구 사항은 아래에 자세히 나열되어 있습니다.

또한 Forward+, PBR(Physical Based Rendering), Unity(Legacy Deferred Rendering) 등의 렌더링 방법이 있는데 여기서는 자세히 설명하지 않습니다. 관심이 있으시면 정보를 찾아보실 수 있습니다.

4.7 장면 관리 및 오클루전 컬링

4.7.1 장면 관리

장면 관리는 게임 장면에서 공간적 속성을 가진 모든 개체입니다. 목적은 객체를 빠르게 찾고, 객체 업데이트를 줄이고, 물리적 충돌 속도를 높이고, 렌더링 시 폐색을 제거하는 것입니다. 일반적인 장면 관리 방법에는 BSP(Binary Space Split Tree)/Quadtree(평면 공간)/Octree(3차원 공간)/Portail이 있습니다.

위 그림의 왼쪽은 쿼드트리를 나타내고, 그림의 오른쪽은 옥트리를 나타냅니다. 여기서는 구체적인 원칙과 구현 방법을 설명하지 않습니다. 궁금하신 분들은 따로 검색해 보세요.

4.7.2 오클루전 컬링

오클루전 컬링 기술은 카메라의 뷰포트 내에 없는 개체를 제거하고 처리를 위해 렌더링 파이프라인으로 보내지 않으므로 많은 렌더링 개체가 줄어듭니다.

위의 왼쪽 그림은 오클루전 컬링을 하지 않은 장면이고, 오른쪽 그림은 오클루전 컬링을 활성화한 후의 장면입니다. 개체의 절반 이상이 제거되어 렌더링되고 최적화 효과가 매우 분명하다는 것을 알 수 있습니다. 장면 관리(4.5) 방법과 결합하여 빠른 제거 알고리즘을 구현할 수 있습니다. Unity의 오클루전 컬링에는 Occluder 및 Occludee 설정이 필요합니다. 자세한 내용은 여기를 참조하세요.

4.8 그림자

다양한 비용과 효과로 그림자를 구현하는 방법에는 여러 가지가 있습니다.

4.8.1 텍스처 그림자

매핑하는 가장 간단한 방법은 그림자 텍스처를 만들어 개체의 발 부분에 배치하고(아래 그림) 개체의 움직임을 따라가는 것입니다.

지도 그림자 렌더링은 매우 간단하여 두 개의 삼각형만 필요하며 저가형 모델에 적합합니다. 지면이 기복이 있고 텍스처가 지면에 의해 가려지는 경우 데칼 기술을 사용하여 그림자 맵을 지면에 부착할 수 있습니다. 그러나 데칼은 여전히 ​​다른 동적 개체에 의해 비정상적으로 차단될 수 있습니다.

4.8.2 프로젝터

프로젝터 기술은 광원 위치와 프러스텀(Frustum)를 미리 지정한 후 다른 개체에 대한 개체의 투영을 계산합니다. (아래 사진)

객체를 어떤 평면에나 투영할 수 있지만 지도 그림자처럼 투영된 객체의 윤곽선을 표현할 수는 없습니다. 중간 품질 효과에 적합합니다.

4.8.3 그림자 맵

그림자 맵 기술은 렌더링을 위해 물체를 빛 공간(위 그림)에 넣고 빛 공간의 깊이 맵(그림자 맵이라고도 함)을 얻는 것입니다. 그런 다음 일반 렌더링 중에 빛 공간의 특정 조각의 깊이를 그림자 맵과 비교하면 조각이 그림자에 있는지 여부를 판단할 수 있습니다(아래 그림).

Shadow Map 기술은 어떤 평면에서도 객체의 투영을 렌더링할 수 있습니다. 렌더링 효과는 실제 세계에 가장 가깝지만(아래 그림), 성능 소모는 훨씬 더 높습니다. Draw Calls가 늘어나고 비디오 메모리 사용량이 늘어납니다. 일반적으로 고급 모델이나 중요한 역할에 적합합니다.

4.8.4 그림자 그레이딩 전략

ShadowMap은 효과가 가장 좋지만 성능을 가장 많이 소비하는 반면 매핑 방법은 정반대입니다. 이를 위해서는 프로젝트의 특정 조건을 기반으로 그레이딩 전략을 수립하고 이미지 품질과 중요도가 다른 개체에 대해 다양한 그림자 렌더링 방법을 사용해야 합니다. 다음 표는 Game Z의 등급 전략입니다.

4.9 대역폭 최적화

대역폭 최적화의 목적은 CPU와 GPU 간의 데이터 전송을 줄이는 것입니다.

4.9.1 LOD(세부 수준)

LOD는 세부 수준입니다. 렌더링 및 대역폭 소비를 줄이기 위해 화면의 개체 크기에 따라 다양한 수준의 리소스가 선택됩니다. LOD는 그래픽 렌더링에 널리 사용됩니다. 적용 가능한 객체에는 모델 LOD, 표면 LOD, 재료 LOD, 식생/나무 LOD, 조명 LOD, 텍스처 LOD(MipMaps) 등이 있습니다. 아래 그림은 객체가 카메라에서 점점 멀어지는 모습과 다양한 LOD 객체를 사용하는 모습을 보여줍니다. 그 중 LOD0의 정확도가 가장 높고 LOD2의 정확도가 가장 낮습니다.

Unity 엔진은 LODGroup 구성 요소를 통해 모델 LOD를 구현합니다. (아래 사진)

4.9.2 GPU 인스턴스

GPU 인스턴스 기술은 동일한 메시의 여러 인스턴스를 그리는 데 사용되며 각 인스턴스는 고유한 매개변수(예: 색상/위치/스케일 등)를 가질 수 있습니다. 이 기술은 대역폭을 줄일 수 있으며 하나의 메시 데이터만 전달하면 원하는 수의 인스턴스를 그릴 수 있습니다. 일반적으로 건물, 표면 장식, 입자, 잔디, 나무, 초목 등을 그리는 데 사용됩니다.

위 사진의 장면은 총 1,900개 이상의 볼이 있는데 배치번호는 24개에 불과합니다. GPU 인스턴스를 켠 효과입니다. 그러나 플랫폼에 대한 특정 요구 사항이 있습니다.

4.9.3 GPU 스킨

CPU 스킨은 캐릭터 애니메이션을 구현하는 가장 기본적인 방법입니다. 융합/페이딩/결합/연결 등 매우 복잡한 애니메이션 작업을 쉽게 구현할 수 있습니다. 그러나 단점도 분명합니다. CPU 컴퓨팅 성능을 많이 차지하며 병렬 계산을 수행하기가 어렵습니다. 각 프레임은 모델 버텍스 데이터를 GPU로 전송해야 하므로 대역폭 부하가 증가합니다.

GPU Skin 방법은 뼈대 매트릭스 목록을 균일하게 Shader에 전달한 다음 Vertex Shader에서 모델 정점에 대한 스키닝 계산을 수행하는 것입니다. 병렬 계산을 수행하고 CPU 부하를 줄일 수 있습니다. 또한, 각 프레임에서 GPU로 전달되는 데이터는 버텍스 데이터가 아닌 뼈대 매트릭스이기 때문에 대역폭 부하가 크게 줄어듭니다.

물론 GPU 스킨에도 결함이 있습니다. GPU 로드가 증가하고, 최대 뼈대 수가 플랫폼에 따라 제한되는 경우가 많으며(예: OpenGL ES 2.0은 Vector4 데이터 볼륨 250개를 초과할 수 없음), 복잡한 애니메이션 작업에 도움이 되지 않습니다. Unity 엔진은 PlayerSettings 패널에서 GPU 스킨을 활성화할 수 있습니다(아래 그림).

또한 GPU 스킨을 GPU 인스턴스 기술과 결합하여 동일한 캐릭터의 수많은 골격 애니메이션을 렌더링할 수도 있습니다. 자세한 내용은 여기를 참조하세요.

4.9.4 GPU 입자

GPU 파티클의 장점과 단점은 GPU 스킨과 유사합니다. 파티클의 병렬 컴퓨팅을 지원하고 대역폭 부하를 줄이지만 파티클의 고급 특성(소프트 파티클/충돌 등)을 구현하는 것도 어렵습니다. Unity는 모델 파티클을 특별히 최적화했으며 GPU 인스턴싱을 지원합니다(아래 그림).

4.9.5 버퍼 태그의 올바른 사용

OpenGL의 버퍼 태그에는 다음이 포함됩니다.

GL_STREAM_DRAW

GL_STREAM_READ

GL_STREAM_COPY

GL_STATIC_DRAW

GL_STATIC_READ

GL_STATIC_COPY

GL_DYNAMIC_DRAW

GL_DYNAMIC_READ

GL_DYNAMIC_COPY

  1. DRAW: 그리기를 위해 버퍼 데이터가 GPU로 전송됩니다.

  2. READ: CPU 애플리케이션이 버퍼 데이터를 읽습니다.

  3. COPY: 버퍼 데이터는 그리기 및 읽기에 사용됩니다.

  4. STATIC: 한 번 수정하고 여러 번 사용합니다. 정적 모델 버텍스 데이터에 사용할 수 있습니다.

  5. DYNAMIC: 여러 번 수정하고 여러 번 사용했습니다. 애니메이션 모델 버텍스 데이터 및 입자 시스템 데이터에 사용할 수 있습니다.

  6. STREAM: 여러 번 수정하고 한 번만 사용하세요. 에디터 데이터 등 특수한 상황에서 사용할 수 있습니다.

버퍼 태그에는 각각 용도가 있으므로 적절한 유형을 선택하면 대역폭 로드와 비디오 메모리 사용량을 줄일 수 있습니다.

4.10 셰이더 최적화

  • 시간이 많이 걸리는 수학 연산을 사용하지 마십시오. pow, exp, log, sin, cos, tan 등과 같은

  • 정밀도가 낮은 부동 소수점 숫자를 사용하십시오. OpenGL ES의 부동 소수점 숫자에는 highp(32비트 부동 소수점), Mediump(16비트 부동 소수점) 및 lowp(8비트 부동 소수점)의 세 가지 정밀도가 있습니다. 많은 계산에는 높은 정밀도가 필요하지 않으며 정밀도가 낮은 부동 소수점으로 변경할 수 있습니다.

hlsl
precision mediump float; // Defines precision for float and float-derived (vector/matrix) types.
uniform lowp sampler2D sampler; // Texture2D() result is lowp.
varying lowp vec4 color;
varying vec2 texCoord;   // Uses default mediump precision.
  • 폐기 작업을 비활성화합니다. 그 이유는 4.2.2를 참고하세요.

  • 중복 계산을 피하세요.

cpp
precision mediump float;
float a = 0.9;
float b = 0.6;

varying vec4 vColor;

void main()
{
    gl_FragColor = vColor * a * b; // a * b개 픽셀(Pixel)계산/산출(Calculate),의 。
}
  • 벡터 지연 계산.
cpp
highp float f0, f1;
highp vec4 v0, v1;

v0 = (v1 * f0) * f1; // v1 및 f0계산/산출(Calculate)반환(Return)개 벡터 , 및 f1계산/산출(Calculate),회 벡터 계산/산출(Calculate)。
// :
v0 = v1 * (f0 * f1); // 계산/산출(Calculate)개 포인트 ,오직 벡터 계산/산출(Calculate)회 。
  • 벡터 구성요소 마스크를 최대한 활용하세요.
cpp
highp vec4 v0;
highp vec4 v1;
highp vec4 v2;
v2.xz = v0 * v1; // v2오직 을(를) 활용하여 xz,v2 = v0 * v1의 。
  • 배열 첨자를 계산하지 마세요. 셰이더에서 동적 첨자를 사용하면 상당한 오버헤드가 발생합니다.

  • 동적 텍스처 샘플링(동적 텍스처 조회, 종속 텍스처 읽기라고도 함)에 주의하세요. 즉, 셰이더에서 텍스처 좌표가 변경되는데, 이는 동적 텍스처 샘플링(종속 텍스처 읽기라고도 함)입니다. OpenGL ES 2.0 아키텍처에서는 동적 텍스처 샘플링으로 인해 심각한 성능 문제가 발생합니다. 3.0에는 이런 문제가 없습니다. 자세한 내용은 여기를 참조하세요.

cpp
varying vec2 vTexCoord;
uniform sampler2D textureSampler;

void main()
{
    vec2 modifiedTexCoord = vec2(1.0 - vTexCoord.x, 1.0 - vTexCoord.y); // 텍스처(Texture)좌표
    gl_FragColor = texture2D(textureSampler, modifiedTexCoord);    // 트리거 Dynamic Texture Lookup/Dependent Texture Read。
}
  • 임시 변수를 피하세요.

  • for와 같은 루프 문을 사용하지 마십시오. 확장을 시도할 수 있습니다.

  • Pixel Shader 계산을 Vertex Shader로 이동해 보십시오. 예를 들어 픽셀 빛이 버텍스 빛으로 변경됩니다.

  • 꼭지점이나 픽셀과 관련이 없는 계산을 CPU로 옮긴 다음 이를 유니폼을 통해 전달합니다.

  • 등급 전략. 다양한 이미지 품질과 다양한 플랫폼은 다양한 복잡성의 알고리즘을 사용합니다.

4.11 UI 최적화

리소스 최적화(2.2) 외에도 UI는 렌더링 시 몇 가지 최적화 조치를 취할 수도 있습니다.

  • 서로 다른 아틀라스의 컨트롤이 교차하는 것을 피하세요. 인터리빙은 그리기 호출을 증가시키므로 피해야 합니다.

  • 동적 영역과 정적 영역을 분리합니다. 즉, 텍스트/소품 등 동적 컨트롤은 동일한 레이어에 배치하고, 기타 정적 컨트롤은 최대한 동일한 레이어에 배치해야 일괄 통합 가능성을 높일 수 있습니다.

4.12 기타 렌더링 최적화

4.12.1 포스트 프로세스 방지

후처리는 장면 객체의 렌더링이 완료된 후 렌더링 텍스처에 대해 픽셀 단위 처리를 수행하여 다음 효과를 포함한 다양한 전체 화면 효과를 얻는 것입니다.

  • 안티앨리어싱: 안티앨리어싱(FXAA & TAA)

  • 앰비언트 오클루전(AO): 앰비언트 오클루전(AO)

  • 스크린 스페이스 리플렉션: 스크린 스페이스 리플렉션

  • 안개 : 안개

  • 뎁스 오브 필드(DoF) : 뎁스 오브 필드(DoF)

  • 모션 블러: 모션 블러

  • 인간의 눈 조정: 눈 적응

  • 글로우: 블룸

  • 색상 보정: 컬러 그레이딩

  • 색상 조회 테이블: 사용자 Lut

  • 색수차

  • 곡물 : 곡물

  • 비네트 : 비네트

  • 노이즈: 디더링

Unity의 포스트 프로세싱 스택:

후처리를 통해 멋지고 사실적인 효과를 많이 얻을 수 있지만 소비량을 과소평가해서는 안 됩니다. 특히 모바일 측면에서는 모바일 기기 하드웨어 아키텍처의 특별한 설계로 인해 더욱 심각한 성능 문제가 발생할 수 있습니다. 주요 이유는 다음과 같습니다.

  • 종속 텍스처 읽기 속도가 느려집니다. 종속 텍스처 읽기에 대한 설명은 여기를 참조하세요.

  • 하드웨어 기능이 누락되었습니다.

  • 추가 렌더 텍스처 해상도 비용(추가 렌더 대상 해결 비용).

따라서 모바일 게임에서는 후처리를 사용하지 않도록 노력해야 합니다.

4.12.2 해상도 줄이기

해상도를 줄이는 것은 렌더링 성능을 향상시키는 가장 조잡하고 효과적인 방법입니다. 현재 많은 스마트 장치의 해상도는 초고해상도(종종 2K 이상)이기 때문에 CPU/GPU가 따라잡을 수 없습니다. 원래 화면 해상도를 사용하면 심각한 프레임 정지/드롭이 발생합니다. 일반적으로 화면 해상도를 절반으로 줄일 수 있으므로 렌더링 텍스처/깊이 버퍼/텍스처 및 기타 데이터를 원본의 1/4로 줄일 수 있으며, 이는 CPU/GPU/대역폭의 다양한 지표의 소비를 크게 줄입니다.

5. 메모리 최적화

메모리 최적화의 목적은 IO 속도를 높이고, 메인 스레드가 멈추는 것을 방지하고, 빈번한 메모리 작업(생성/삭제)을 방지하고, 메모리 조각화 및 과도한 점유를 방지하는 것입니다.

5.1 캐싱 방법

CPU의 캐시 계산과 유사하게, 반복적으로 생성되어야 하는 객체를 캐시하고, 소멸되면 캐시 목록에 넣고, 다시 생성되면 먼저 캐시 목록에서 읽어오는 것이 아이디어입니다. 의사코드 구현:

cpp
Array<Object> _objectCache;

Object CreateObject()
{
    // 로부터 캐싱(Cache) 내에서 페치/가져오기(Fetch)객체/오브젝트
    if (_objectCache.size() > 0)
    {
        return _objectCache.pop_back();
    }
    return new Object();
}

void DestroyObject(Object obj)
{
    _objectCache.push_back(obj); // 캐싱(Cache)리스트 。
}

캐싱 방법은 메모리 생성/삭제 빈도를 줄이고 조각화를 방지할 수 있습니다. 미니언, NPC, 체력 바, 특수 효과, 소품, 다양한 아이콘 등과 같이 수가 많고 자주 생성되는 개체에 일반적으로 사용됩니다.

5.2 메모리 풀

메모리 풀 기술은 메모리 조각화를 방지하고 메모리 할당 및 관리 속도를 높이는 것을 목표로 하는 최신 주류 엔진의 표준 기능입니다. 구현 아이디어는 일반적으로 엔진이 더 큰 메모리를 미리 생성하는 것입니다(동적으로 조정될 수도 있음). 이 메모리는 효과적인 데이터 구조와 알고리즘 전략을 통해 작은 메모리 블록의 할당 및 재활용을 균일하게 관리하고 논리 계층에 메모리 관련 작업 인터페이스를 제공합니다. 메모리 풀을 구현하는 방법에는 여러 가지가 있으며 각각 고유한 장점과 단점이 있습니다. 아래 그림은 구현 방법 중 하나입니다.

할당된 메모리는 네 부분으로 나뉩니다. 부분 1은 메모리 풀 구조 정보입니다. 파트 2는 메모리 매핑 테이블입니다. 파트 3은 메모리 청크 버퍼입니다. 파트 4는 할당 가능한 메모리 영역입니다. 여기에서 자세한 내용을 확인하세요.

5.3 리소스 관리자

리소스 관리자는 생성, 로드, 해제, 재활용 등 통합적으로 사용해야 하는 모든 파일 리소스를 관리합니다. 재사용률을 높이고 리소스 중복성과 메모리 오버헤드를 줄이기 위해 현대 엔진에도 필수적인 모듈입니다. 리소스 관리자가 없으면 필연적으로 리소스 중복이 발생하며 동일한 리소스에 메모리 데이터의 복사본이 많이 있을 수 있습니다(아래 그림).

위 그림과 같이 각 모델(Model)은 메시 메모리 데이터 복사본 1개를 참조하고, 모델 인스턴스 3개에는 메시 메모리 데이터 복사본 3개가 있어 메시 메모리 리소스가 중복됩니다. 리소스 관리자의 통합 관리를 통해 파일 리소스를 참조하는 모든 인스턴스는 동일한 메모리 데이터(아래 그림)를 가리키므로 메모리 리소스 중복을 방지하고 메모리 및 IO 소비를 줄입니다.

리소스 관리자의 구현은 상대적으로 간단합니다. 주로 템플릿을 사용하여 개체 유형을 추상화한 다음 각 개체 유형을 map<filePath, objectData> 테이블에 저장합니다. 여기서는 구체적인 구현에 대해 설명하지 않습니다.

5.4 제어 GC

GC는 Garbage Collect의 약자로 쓰레기 수집을 의미합니다. 메모리 풀이나 관리되는 힙의 캐시 영역에서 쓸모 없는 메모리와 쓸모 없는 개체를 재활용하기 위해 특정 전략을 사용하는 게임 엔진의 기술입니다. GC 메커니즘은 너무 오랫동안 과도한 메모리 사용을 방지하는 것입니다. 메모리 사용량을 자동으로 조정하는 일반적인 기술입니다. GC 트리거는 일반적으로 두 가지 유형으로 나뉩니다.

  • 엔진 트리거. 일반적으로 시간 간격에 도달하거나 메모리 사용량이 특정 임계값에 도달하면 엔진이 GC를 트리거합니다.

  • 사용자 호출. 일반적으로 엔진은 논리 계층이 GC 타이밍을 제어할 수 있도록 게임 애플리케이션용 API도 제공합니다. 예를 들어 Unity의 GC.Collect() 인터페이스는 GC 작업을 트리거할 수 있습니다.

그러나 GC를 트리거하려면 메모리 풀/관리되는 힙/다양한 캐시 테이블을 순회해야 하며 메모리 조각 모음 작업도 트리거할 수 있으므로 일정량의 CPU 성능이 필요하며 프레임 드롭 및 정지의 원인 중 하나입니다. 그런 다음 GC 트리거를 방지하거나 GC 트리거 처리 시간을 줄이기 위해 논리 계층에서 몇 가지 방법을 채택해야 합니다. 일반적으로 사용되는 방법:

  • 잦은 생성/삭제를 피하세요. 이것은 이해하기 쉽습니다. 객체를 자주 생성하고 삭제하면 메모리 조각화가 많이 발생하고 쓸모없는 객체가 발생하여 GC가 발생할 확률과 시간이 늘어납니다.

  • 프레임 업데이트 내에서 임시 개체 및 메모리 생성을 피하십시오.

  • for/while 루프에서 임시 객체와 메모리 생성을 피하세요.

  • 많은 양의 메모리를 할당하지 마십시오. 큰 메모리 블록을 적용하면 메모리가 급증하여 GC 확률이 높아집니다.

  • 메모리 누수를 피하세요. 이를 위해서는 각 기술자의 전문적인 기술과 인식이 필요합니다. 일부 보조 도구를 통해서도 메모리 누수를 확인할 수 있습니다. 자세한 내용은 1.3을 참조하세요.

  • 적극적으로 GC를 호출합니다. 예를 들어 전투 시작 전과 후에 GC를 호출하고, 장면을 전환하고, 메인 인터페이스를 전환하면 메모리 사용량을 어느 정도 줄이고 프레임 드롭/스터터링을 방지할 수 있습니다.

5.5 논리 최적화

로직 최적화의 목표는 쓸모없는 메모리 작업을 피하고, 메모리 누수를 방지하고, 가능한 한 빨리 메모리를 해제하고, 전역 변수의 사용을 줄이고, 타사 라이브러리의 메모리 소비에 주의를 기울이는 것입니다.

6. Caton 최적화

나는 많은 개발자나 플레이어가 이런 상황에 직면했다고 생각합니다. 게임은 대부분 원활하게 실행되지만 전투 중 특정 순간이나 특정 인터페이스를 열 때 정지되거나 심지어 오랫동안 정지되기도 합니다. 이 현상이 막혔습니다. 지연되는 데에는 여러 가지 이유가 있지만 주요 원인은 다음과 같습니다.

  • 대량의 IO가 발생합니다.

  • 단기 대용량 메모리 작업.

  • 렌더링된 객체가 갑자기 폭발합니다.

  • GC를 트리거합니다.

  • 많은 양의 리소스가 포함된 장면이나 인터페이스를 로드합니다.

  • 너무 복잡한 논리를 유발합니다.

지체를 방지하거나 완화하는 기술도 위의 이유를 중심으로 이루어집니다.

6.1 프레임 축소 방법

3.3의 방법과 유사하게 업데이트 빈도를 강제로 줄여 지연 시간을 늦추는 것입니다.

6.2 프레임 확산 방법

프레임 확산 방식은 원래 동일한 프레임에서 처리해야 하는 로직을 여러 부분으로 나누어 여러 프레임에 분산시켜 처리함으로써 동일한 프레임의 처리 시간을 단축하고 지연 현상을 늦추는 방식입니다. 예를 들어, 동일한 프레임에 10개의 미니언을 생성해야 하므로 지연이 발생할 수 있는 경우 각 프레임에 2개의 미니언만 생성하여 5프레임에 걸쳐 분산시킬 수 있습니다. 이 방법은 리소스 로딩, AI 업데이트, 물리 업데이트, 시간이 많이 걸리는 논리 처리 등에 적용됩니다. 또한 지연을 방지하기 위해 전처리(3.2)와 기본 및 보조 방법(3.4)을 사용할 수도 있습니다.

6.3 수량 제한 방법

프레임 감소 ​​방법, 프레임 확산 방법, 전처리, 1차 및 2차 방법으로 문제를 해결할 수 없고 지연의 원인이 너무 많은 개체로 인해 발생한 경우 개수를 제한하는 것이 매우 필요합니다. 방법은 매우 간단합니다. 장면에 생성된 특정 객체(캐릭터, 특수효과, 체력바 등)의 개수가 최대치에 도달하면 강제로 다시 생성되지 않도록 합니다. 이 방법은 일부 논리 오류 및 좋지 않은 게임 경험을 유발할 수 있으므로 주의해서 사용하고 처리해야 합니다.

6.4 논리 최적화

논리가 너무 복잡하여 지연이 발생하는 경우 논리를 목표 방식으로 최적화해야 합니다. 각 프로젝트의 논리는 다르며 여기서는 구체적인 최적화 조치를 제공할 수 없습니다.

6.5 IO 최적화

느린 IO로 인해 게임이 정지되어 메인 스레드가 대기하는 것은 매우 흔한 일입니다. 다음은 일반적으로 사용되는 몇 가지 최적화 기술입니다.

6.5.1 사전 로드

시간이 많이 걸리는 IO를 특정 순간(게임 시작 시, 장면 로드 시, 메인 인터페이스 진입 시 등)에 미리 로드합니다. 예를 들어, 일부 캐릭터는 큰 리소스를 가지고 있어 전투 중 지연을 방지하기 위해 전투 장면을 로드할 때 미리 로드할 수 있습니다.

6.5.2 비동기 로딩

메인 스레드를 차단하지 않으려면 IO를 비동기식으로 만드세요. 이 기술의 적용은 매우 일반적이므로 다시 설명하지 않겠습니다.

6.5.3 압축된 리소스

원래 흩어져 있던 파일을 하나의 파일로 압축하거나 특정 알고리즘(예: 허프만 코딩)을 사용하여 대용량 파일을 압축하여 파일 크기를 줄입니다. 이를 통해 IO 시간도 줄일 수 있습니다. 물론 리소스를 압축하면 부작용도 있습니다. 더 많은 메모리가 필요하며 압축 해제 프로세스에도 추가 CPU가 소비됩니다.

6.5.4 다중 레벨 캐시

우리 모두는 CPU 주파수가 가장 높다는 것을 알고 있습니다. 현재 가정용 PC의 주요 주파수는 3.2GHz 이상에 달할 수 있습니다. CPU에는 L1L3 캐시가 있으며 속도는 약간 다릅니다. 메모리의 액세스 속도는 CPU의 액세스 속도보다 훨씬 낮으며 일반적으로 23GHz로 CPU의 약 1/10입니다. 하드 디스크의 액세스 속도는 메모리의 액세스 속도보다 훨씬 낮으며 일반적으로 0.1Gb/s로 메모리 읽기 속도보다 훨씬 낮습니다. 네트워크는 더욱 느려집니다. 현재 광섬유조차도 0.02Gb/s에 불과하다. 일반적으로 우리가 제어할 수 있는 것은 메모리/디스크 및 네트워크의 데이터이므로 속도에 주의를 기울이면 속도 관계는 대략 다음과 같습니다.

따라서 다단계 캐싱 전략이 등장했습니다. 이 방법은 의사 코드를 구현하기 위한 추가 디스크 캐싱 계층이 있다는 점을 제외하면 캐싱 방법과 유사합니다.

cpp
map<string, ObjectType> _memoryCache;

ObjectType CreateObject(string objectPath)
{
    // 1. 로부터 메모리 캐싱(Cache) 내에서 로드/읽기(Read),반환(Return)。
    if (_memoryCache.count(objectPath) > 0)
    {
        return _memoryCache[objectPath];
    }

    ObjectType obj = NULL;
    // 2. 로부터 로드 。
    if (FileExisted(objectPath))
    {
        obj = LoadObjectFromFile(objectPath);
        _memoryCache[objectPath] = obj;
        return obj;
    }
    
    // 3. 로부터
    DownloadObjectFromNet(objectPath);
    obj = LoadObjectFromFile(objectPath);
    _memoryCache[objectPath] = obj;
    return obj;
}

6.5.5 제어 로그

게임 로그는 일반적으로 일정 기간 후에 보관됩니다. 로직이 잘 처리되지 않으면 렉이 발생할 가능성이 높습니다. 예를 들어 매 프레임마다 많은 수의 디버깅 로그를 출력하면 아카이브가 자주 트리거됩니다. Z게임 초반에는 렉도 발생했다. 이후 프로파일러 분석 결과 로그 아카이빙으로 인해 발생한 것으로 밝혀졌습니다. 따라서 Log에 대한 일부 최적화가 필요합니다. 일반적인 최적화 방법:

  • 프레임 업데이트 출력 로그를 피하십시오. 로그 데이터의 급격한 확장으로 인해 잦은 아카이빙이 발생하거나 아카이빙 시간이 늘어나는 것을 방지합니다.

  • 향상된 로그 보관 메커니즘. 로그 보관 메커니즘은 얼마나 자주 보관해야 하는지, 로그 데이터가 특정 수준에 도달하는 시기 등 적절하게 개선될 수 있습니다.

  • 로그 수준을 설정합니다. 로그는 정보, 경고, 오류 수준으로 나눌 수 있습니다. 중요하지 않은 로그는 보관되지 않습니다.

  • 비동기식 보관. 로그 보관 로직은 메인 스레드가 멈추는 것을 방지하기 위해 별도의 스레드로 분리됩니다.

  • 쓸모없는 로그는 피하세요. 이를 위해서는 잘못된 로그를 방지하기 위해 논리 계층에서 로그 출력을 제어해야 합니다.

XML 대신 6.5.6 JSON

일반적으로 게임 데이터 저장에는 바이너리 형식과 텍스트 형식의 두 가지 유형이 있습니다. 바이너리 형식은 데이터 양이 가장 적지만 가독성과 확장성이 떨어지며, 모델/텍스처/폰트/오디오 등의 데이터를 저장하는 데 적합합니다. 텍스트 형식의 특성은 바이너리와 정반대이며 구성 정보를 저장하는 데 적합합니다. 가장 일반적인 텍스트 형식은 JSON과 XML입니다. JSON은 XML에 비해 많은 장점이 있습니다.

  • 데이터 양이 적습니다. JSON 형식은 XML보다 40% 더 짧은 시간에 동일한 데이터를 표현할 수 있습니다(아래 참조).
cpp
<?xml version="1.0" encoding="utf-8" ?>
<country>
  <name>中国</name>
  <province>
    <name>福建</name>
    <citys>
      <city>福州</city>
      <city>南平</city>
    </citys>    
  </province>
  <province>
    <name>广东</name>
    <citys>
      <city>广州</city>
      <city>深圳</city>
      <city>梅州</city>
    </citys>   
  </province>
</country>
{
    name: "中国",
    provinces: [
      { name: "福建", citys: { city: ["福州", "南平"]} },
      { name: "广东", citys: { city: ["广州", "深圳", "梅州"]} }
    ]
}
  • 가독성이 향상되었습니다. 위의 두 단락은 각각 XML과 JSON으로 동일한 데이터를 표현합니다. 어느 쪽이 더 읽기 쉬운지 한눈에 알 수 있습니다.

  • 더 빠른 구문 분석. JSON은 데이터 용량이 작기 때문에 IO가 빨라지고, 파싱 속도도 빨라집니다.

각 게임에는 캐릭터 정보, 스킬 정보, 장면 정보, 구성 정보 등 보관해야 할 많은 양의 논리적 데이터가 있습니다. 이러한 데이터가 텍스트 형식으로 저장하기에 적합하다면 의심할 여지 없이 JSON이 첫 번째 선택입니다.

6.6 진행률 표시줄 사용

위의 장 중 어느 것도 지연 문제를 해결할 수 없는 경우 진행률 표시줄을 사용해 볼 수 있습니다. 정체된 로직을 추출해 여러 단계(단계)로 나누자는 아이디어다. 각 단계가 완료되면 UI 진행률 표시줄을 새로 고치기 위해 하나의 프레임이 제공됩니다. 물론 비동기적으로 구현할 수도 있습니다. 의사코드:

cpp
HandleStep1();
RefreshProgressBar(1 / n);
WaitForNextFrame();

HandleStep2();
RefreshProgressBar(2 / n);
WaitForNextFrame();

...

HandleStepN();
RefreshProgressBar(1);
WaitForNextFrame();

7. 전력 소비 최적화

게임 전력 소비와 게임 카드 사이에는 반드시 필요한 연결이 없습니다. 일부 게임은 특정 기기에서 원활하게 실행되지만 전력 소모가 매우 높은 것으로 나타났습니다. 30분 미만 플레이한 후 배터리 알람이 표시되었습니다. 게임 전력 소비의 주요 이유는 일반적으로 높은 CPU 사용량, 빈번한 메모리 작업, 빈번한 디스크 IO 및 일반적으로 높은 렌더링 소비로 인해 높은 대역폭 로드 및 GPU 소비로 이어집니다. 앞서 소개한 장들은 기본적으로 전력 소비를 줄일 수 있는 동시에 전력 소비를 최적화하기 위해 필요한 조치이기도 합니다. 특히, 전력 소비 최적화는 다음 장에서 더욱 분명해집니다.

2. 자원 최적화

3.3 프레임 제한 방법

3.4 1차 및 2차 방법

3.6 엔진 모듈 최적화

4 렌더링 최적화

5.4 제어 GC

6. 5 IO 최적화

위의 장 외에도 최적화 기술을 사용하여 프레임 속도와 이미지 품질을 동적으로 조정할 수도 있습니다.

7.1 프레임 제한을 동적으로 조정

게임 로직은 일반적으로 현재 장치의 성능을 얻을 수 있습니다. 가능하다면 가끔씩 전력 정보를 얻어 단위 전력 소비량을 계산합니다. 단위 시간당 전력 소비가 너무 높고 게임 프레임 속도가 높은 것으로 확인되면(예: 50 이상) 프레임 속도를 적극적으로 줄일 수 있습니다(예: 30).

7.2 이미지 품질을 동적으로 조정

이 방법은 프레임 속도 대신 이미지 품질 수준이 조정된다는 점을 제외하면 동적 프레임 제한과 유사합니다. 물론 조합하여 사용할 수도 있습니다.

8. 네트워크 최적화

네트워크 최적화의 목적은 네트워크 패킷을 더 작게 만들고, 더 신속하게 응답하고, 트래픽을 덜 소비하고, 기본 스레드 차단을 방지하는 것입니다.

8.1 쓸모없는 필드 줄이기

네트워크 패킷에는 일반적으로 캐릭터 위치, 방향, 상태 등과 같은 많은 정보가 포함됩니다. 2.5D 게임인 경우 위치 z 구성 요소를 삭제할 수 있습니다. 방향은 xz 평면에만 있으므로 RotationY만 전송하면 됩니다. 이런 식으로 쓸모없는 필드를 줄임으로써 네트워크 패킷 크기를 어느 정도 줄일 수 있습니다.

8.2 필드 정밀도 감소

일반적으로 로직의 많은 정보는 캐릭터 위치, 방향, 스킬 또는 버프 정보 등을 포함하여 4바이트입니다. 그러나 이 정보는 최대 4바이트에 도달하지 못하고 2바이트 또는 1바이트로 압축될 수 있는 경우가 많습니다. 예를 들어, 동일한 위치에 대해 장면의 크기는 일반적으로 2바이트(-32512~32512)의 표현 범위 내에 있습니다. 해당 위치의 x/y/z를 2바이트로 압축하여 전송할 수 있습니다. 마찬가지로 RotationY 방향은 2바이트로 표시될 수 있습니다.

8.3 반복 전송 방지

게임 네트워크 모듈은 예를 들어 플레이어가 짧은 시간 내에 복권 버튼을 여러 번 누르는 경우와 같이 일부 프로토콜이 짧은 시간 내에 반복적으로 전송되는 것을 효과적으로 제한해야 합니다. 따라서 이를 제한할 수 있는 메커니즘이 필요합니다. 예를 들어, 네트워크 프로토콜을 정의할 때 표시를 추가하여 해당 프로토콜이 특정 기간 내에 반복적으로 전송될 수 없음을 나타낼 수 있습니다.

8.4 네트워크 비동기화

네트워크 프로토콜 패킷의 전송 및 수신을 처리하기 위해 독립 스레드를 생성하는 것은 게임이 메인 스레드와 서로 기다리지 않도록 하기 위한 일반적인 최적화 방법입니다.

8.5 잘못된 바이트 압축

유효하지 않은 바이트를 압축한다는 것은 각 필드의 상위 비트가 모두 0인 데이터를 제거하는 방법을 의미합니다. 예를 들어 문자 레벨 50을 int32로 표현하면 00000000 00000000 00000000 00110010이다. 상위 3바이트는 모두 0이므로 1바이트로 압축할 수 있다. utf8 인코딩과 유사한 바이트를 압축하는 방법이 있습니다.

utf8 인코딩 방법(x는 유효한 비트를 나타냄):

1바이트(최대 유효 비트 7): 0xxxxxxx

2바이트(최대 유효 비트 11): 110xxxxx 10xxxxxx

3바이트(최대 유효 비트 16): 1110xxxx 10xxxxxx 10xxxxxx

4바이트(최대 유효 비트 21): 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

5바이트(최대 유효 비트 26): 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

6바이트(최대 유효 비트 31): 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

예를 들어 문자레벨이 50이면 유효비트는 6이고 1바이트면 충분하며, 이는 00110010으로 압축됩니다(빨간색은 압축 비트 표시). 숫자 1000이면 int32는 00000000 00000000 00000011 11101000으로 표현된다. 유효 비트는 10으로 2바이트의 인코딩이 필요하다. 압축 후에는 11001111 10101000입니다(빨간색은 압축 비트 표시). 이 압축 방법을 사용하면 일반적으로 4바이트의 데이터 대신 1~2바이트를 사용할 수 있으며 압축 효과는 분명합니다.

8.6 압축 프로토콜 패키지

8.5는 현장의 데이터를 압축합니다. 각 데이터 패킷에는 실제로 동일한 숫자가 많이 있습니다. 현재 주류 압축 방법을 사용하여 네트워크 패킷을 다시 압축할 수 있습니다. 게임에 가장 일반적으로 사용되는 압축 방식은 zlib 오픈소스 라이브러리이며, lz4 방식도 사용할 수 있습니다. 특정 구현에 대한 추가 정보를 찾을 수 있으며 여기에서는 자세히 설명하지 않습니다.

9. 요약

이렇게 보면 이 글의 내용은 기본적으로 종료됩니다. 이 기사가 여러분에게 게임 개발을 위한 실질적인 최적화 기술이나 새로운 아이디어를 제공할 수 있기를 바랍니다. 물론 성능 최적화에 대한 몇 가지 일반적인 오해에 대해서도 이야기해야 합니다.

  • 작은 파일이 포함된 사진도 메모리를 덜 차지합니다

이미지 파일이 작으면 메모리를 덜 차지할 것이라고 생각하는 분들도 계시기 때문에, 이미지를 고압축 jpg 포맷으로 변환하시는 분들도 계십니다.

그렇다면 이러한 관점과 접근법이 적절한가? Chapter 2.1에 따르면 이미지가 차지하는 메모리 크기는 이미지의 크기 및 픽셀 형식과 관련이 있으며 파일 형식과는 아무런 관련이 없음을 알 수 있습니다. 따라서 이미지를 jpg로 변환하면 파일 크기만 압축될 뿐 메모리 오버헤드는 줄어들지 않습니다!

  • 배치 데이터가 클수록 좋습니다

어떤 사람들은 일괄 처리로 렌더링 소비를 줄일 수 있으므로 일괄 처리 후 데이터를 더 크게 만들어 그리기 호출을 더 줄이는 것이 더 낫다고 생각할 수도 있습니다.

대답은 '아니요'입니다. 두 가지 이유가 있습니다:

  1. 모바일 게임의 모델 인덱스는 일반적으로 최적화되어 있으며 16비트로만 표현됩니다. 즉, 일괄 처리 후 버텍스 수가 65535개를 초과하면 경계를 넘어 렌더링 예외가 발생하게 됩니다.

  2. 데이터 볼륨이 너무 많으면 LOD, 오클루전 컬링 및 기타 기술을 최대한 활용하지 못하여 GPU로 너무 많은 데이터가 전송되어 대역폭과 GPU 소비가 증가할 수 있습니다.

  • 조각은 픽셀과 동일합니다

프래그먼트는 GPU 내부의 기하학적 구조를 래스터라이제이션하여 형성된 가장 작은 표현 단위입니다. 일련의 조각 작업(알파 테스트, 깊이 테스트, 템플릿 테스트 등)을 거친 후 최종적으로 렌더링 텍스처에 기록되어 픽셀이 될 수 있습니다. 따라서 조각은 픽셀이 아니지만 픽셀이 될 확률을 갖는다.

특별 지침

  • 일부 사진은 인터넷에서 가져온 것이므로 삭제했습니다.

  • 이 글의 링크를 자유롭게 공유해주세요. 저의 동의 없이 전재하는 것을 금지합니다.








📚 참고 자료 및 공식 링크 (References)


🔗 연관 지식 베이스 (Wiki & Tools)

Based on the legendary technical series by Timlly (0向往0 / 毛星云)