2021 LTS의 셰이더 빌드 시간 및 메모리 사용량 개선

Unity의 SRP(스크립터블 렌더 파이프라인)가 점점 더 많은 기능을 제공하게 되면서, 빌드 시 처리하고 컴파일하는 셰이더 배리언트의 양도 증가하고 있습니다. 추가 그래픽스 API에 대한 지속적인 지원 및 타겟 플랫폼의 증가와 더불어 SRP 개선 사항도 지속적으로 확장되고 있습니다.
셰이더가 초기('클린') 빌드 후에 컴파일되고 캐시되므로, 추가 증분('웜') 빌드의 속도를 높일 수 있습니다. 클린 빌드에 걸리는 시간이 가장 긴 것은 일반적이지만, 웜 빌드 시간이 길어지는 것은 프로젝트 개발 및 반복 작업 과정에서 흔히 발생하는 문제점입니다.

유니티의 Shader Management 팀은 의미 있고 확장 가능한 솔루션을 제공하여 이러한 문제를 해결하고자 많은 노력을 기울여 왔습니다. 그 결과 Unity 2021 LTS 이상 버전으로 제작한 프로젝트의 경우, 셰이더 빌드 시간과 런타임 메모리 사용량이 상당히 감소했습니다.
영향을 받는 버전, 백포트, 내부 테스트 수치 등 새로운 최적화에 대해 자세히 알아보려면 셰이더 배리언트 프리필터링 및 동적 셰이더 로딩 섹션으로 바로 건너뛰세요. 이 블로그의 마지막 부분에서는 프로젝트 저작, 빌드, 런타임 전반에 걸쳐 셰이더 배리언트 관리를 더욱 개선하기 위한 향후 계획도 소개합니다.
Unity 셰이더 시스템의 흥미로운 개선 사항을 살펴보기 전에 조건부 셰이더 컴파일, 셰이더 배리언트, 셰이더 배리언트 스트리핑의 개념을 간단히 살펴볼 기회를 갖도록 하겠습니다.
개발자와 아티스트는 조건부 셰이더 기능을 통해 스크립트, 머티리얼 설정뿐만 아니라 프로젝트 및 그래픽스 설정을 사용하여 셰이더의 기능을 편리하게 제어하고 변경할 수 있습니다. 이러한 조건부 기능을 활용해 프로젝트 저작을 간소화할 수 있으므로, 저작 및 유지 관리해야 하는 셰이더의 수를 최소화하여 프로젝트를 효율적으로 확장할 수 있습니다.

조건부 셰이더 기능은 다양한 방법으로 구현할 수 있습니다.
- 정적(컴파일 시) 브랜치
- 셰이더 배리언트 컴파일
- 동적(런타임) 브랜치
정적 브랜칭은 런타임에 브랜칭 관련 셰이더 실행 오버헤드를 피할 수 있지만, 컴파일 시간에 평가 및 잠기며 런타임 제어 기능을 제공하지 않습니다. 한편 셰이더 배리언트 컴파일은 추가적인 런타임 제어 기능을 제공하는 정적 브랜칭의 한 형태입니다. 런타임에 최적의 GPU 성능을 유지하기 위해, 가능한 모든 정적 브랜치 조합에 대해 고유한 셰이더 프로그램(배리언트)을 컴파일하는 방식입니다.
이러한 배리언트는 셰이더 기능을 조건부로 선언하고 평가하여 shader_feature 및 multi_compile 셰이더 키워드를 통해 만들어집니다. 이 경우 활성 키워드와 런타임 설정에 따라 런타임에 올바른 셰이더 배리언트가 로드됩니다. 추가 셰이더 키워드를 선언하고 평가하면 빌드 시간, 파일 크기, 런타임 메모리 사용량이 증가할 수 있습니다.
동시에 동적(유니폼 기반) 브랜칭은 셰이더 배리언트 컴파일의 오버헤드를 완전히 방지하여 빌드 속도를 높이고 파일 크기와 메모리 사용량을 모두 줄입니다. 이러한 방식으로 개발 중에 반복 작업을 더 원활하고 빠르게 진행할 수 있습니다.
반면, 동적 브랜치는 셰이더의 복잡도와 타겟 디바이스에 따라 셰이더 실행 성능에 큰 영향을 줄 수 있습니다. 브랜치의 한쪽이 다른 한쪽보다 훨씬 복잡한 비대칭 브랜치는 성능에 부정적인 영향을 줄 수 있습니다. 셰이더를 더 간단한 경로로 실행해도 더 복잡한 경로에서 성능 저하가 발생할 수 있기 때문입니다.
자체 셰이더에 조건부 셰이더 기능을 도입할 때는 이러한 접근 방식과 장단점을 염두에 두어야 합니다. 자세한 내용은 셰이더 조건부, 셰이더 분기 및 셰이더 배리언트 문서를 참조하세요.
셰이더 배리언트 스트리핑은 셰이더 처리 및 컴파일 시간이 늘어나는 현상을 줄이기 위해 사용됩니다. 셰이더 배리언트 스트리핑의 목적은 다음과 같은 요소에 따라 컴파일에서 불필요한 셰이더 배리언트를 제외하는 것입니다.
- 포함된 머티리얼과 활성화된 키워드
- 프로젝트 및 렌더 파이프라인 설정
- 스크립터블 스트리핑
셰이더 배리언트를 열거할 때 에디터는 빌드에 참조되고 포함된 머티리얼에서 활성화되지 않은 shader_feature로 선언된 키워드를 자동으로 필터링합니다. 결과적으로 이러한 키워드는 추가 배리언트를 생성하지 않습니다.
예를 들어, 복합 조명 URP 셰이더를 사용하는 머티리얼에서 클리어 코트 머티리얼 프로퍼티가 활성화되지 않은 경우 클리어 코트 기능을 구현하는 모든 셰이더 배리언트는 빌드 시점에 안전하게 제거됩니다.
한편 멀티컴파일 키워드는 개발자와 플레이어가 사용 가능한 플레이어 설정과 스크립트를 기반으로 런타임에 셰이더의 기능을 자유롭게 제어할 수 있도록 합니다. 반대로 이러한 키워드는 에디터에서 shader_feature 키워드와 같은 수준으로 자동 제거할 수 없다는 단점이 있습니다. 따라서 일반적으로 더 많은 배리언트를 생성합니다.
스크립터블 스트리핑은 런타임에 필요하지 않은 키워드와 조합을 통해 빌드 시간 동안 셰이더 배리언트를 컴파일에서 제외할 수 있는 C# API입니다. 렌더 파이프라인은 스크립터블 스트리핑을 활용하여 프로젝트의 렌더 파이프라인 설정과 빌드에 포함된 퀄리티 에셋에 따라 불필요한 배리언트를 제거합니다.
저품질 고품질 배리언트 멀티플라이어 메인 라이트/캐스트 섀도: 꺼짐 2x 메인 라이트/캐스트 섀도 켜기: 메인 라이트/캐스트 섀도 1x 켜기: 꺼짐 꺼짐 1x
에디터의 셰이더 배리언트 스트리핑 효과를 극대화하려면 런타임에 활용되지 않는 모든 그래픽스 관련 기능과 렌더 파이프라인 설정을 비활성화하는 것이 좋습니다. 셰이더 배리언트 스트리핑에 대한 자세한 내용은 공식 문서를 참조하세요.
셰이더 배리언트 스트리핑은 빌드의 렌더 파이프라인 퀄리티 에셋과 같은 요소에 따라 컴파일된 셰이더 배리언트의 양을 크게 줄여줍니다. 하지만 현재 스트리핑은 셰이더 처리 단계의 마지막에 수행됩니다. 가능한 모든 배리언트를 단순히 열거하는 작업은 컴파일과 상관없이 여전히 많은 시간이 소요될 수 있습니다.
유니티는 셰이더 배리언트 처리 및 프로젝트 빌드 시간을 줄이기 위해 엔진의 빌트인 셰이더 배리언트 스트리핑에 상당한 최적화 기능을 추가했습니다. 셰이더 배리언트 사전 필터링을 사용하면 클린 빌드와 웜 빌드 시간을 크게 단축할 수 있습니다.
이 최적화는 렌더 파이프라인 설정에 의해 구동되는 사전 필터링 속성에 따라 다중 컴파일 키워드를 조기에 제외하는 방식으로 작동합니다. 이렇게 하면 잠재적인 스트리핑 및 컴파일을 위해 열거되는 배리언트의 양이 줄어들어 셰이더 처리 시간이 단축되며, 가장 극단적인 예시에서는 웜 빌드 시간이 최대 90%까지 단축됩니다.
셰이더 배리언트 프리필터는 2023.1.0a14에 처음 도입되었으며, 2022.2.0b15 및 2021.3.15f1로 백포트되었습니다.


같은 원칙을 적용하면 배리언트 사전 필터링은 초기/클린 빌드 시간 단축에도 도움이 됩니다.


그동안 Unity 런타임은 씬과 리소스를 로드하는 동안 모든 셰이더 오브젝트를 디스크에서 CPU 메모리에 우선적으로 로드했습니다. 대부분의 경우, 구축된 프로젝트와 씬에는 애플리케이션 런타임 동안 특정 시점에 필요한 것보다 더 많은 셰이더 배리언트가 있습니다. 많은 양의 셰이더를 사용하는 프로젝트라면 런타임 시 셰이더 메모리 사용량이 증가하는 경우가 많습니다.
동적 셰이더 로드는 셰이더 로드 동작과 메모리 사용량에 대해 세분화된 사용자 제어 기능을 제공하여 이 문제를 해결합니다. 이 최적화를 통해 셰이더 데이터 청크를 메모리로 스트리밍할 수 있을 뿐만 아니라, 런타임에 더 이상 필요하지 않은 셰이더 데이터를 사용자가 제어하는 메모리 할당량에 따라 제거할 수 있습니다. 이를 통해 메모리 할당량이 제한된 플랫폼에서 셰이더 메모리 사용량을 상당히 줄일 수 있습니다.
이제 에디터의 플레이어 세팅에서 새로운 셰이더 배리언트 로딩 세팅에 액세스할 수 있습니다. 이 설정을 사용하여 로드되는 최대 셰이더 청크 수와 셰이더당 청크 크기(MB)를 오버라이드할 수 있습니다.

이제 다음 C# API를 사용할 수 있으므로, 아래와 같은 에디터 스크립트를 사용하여 Shader Variant Loading Settings를 오버라이드할 수 있습니다.
- PlayerSettings.SetDefaultShaderChunkCount 와 PlayerSettings.SetDefaultShaderChunkSizeInMB 를 사용하여 프로젝트의 기본 셰이더 로딩 세팅을 재정의할 수 있습니다.
- PlayerSettings.SetShaderChunkCountForPlatform 와 PlayerSettings.SetShaderChunkSizeInMBForPlatform를 사용하여 플랫폼별로 이러한 세팅을 재정의할 수 있습니다.
런타임에 로드되는 최대 셰이더 청크의 양을 오버라이드하려면 다음을 통해 C# API를 사용할 수도 있습니다. Shader.maximumChunksOverride. 이를 통해 런타임에 쿼리된 총 사용 가능한 시스템 및 그래픽 메모리와 같은 요소를 기반으로 셰이더 메모리 예산을 재정의할 수 있습니다.
다이내믹 셰이더 로딩은 2023.1.0a11에 출시되었으며 2022.2.0b10, 2022.1.21f1 및 2021.3.12f로 백포트되었습니다 . 유니버설 렌더 파이프라인 (URP)의 보트 어택의 경우 셰이더의 런타임 메모리 사용량이 315MB(기본값)에서 66.8MB(동적 로딩)로 78.8% 감소한 것으로 나타났습니다. 이번 최적화에 대한 자세한 내용은 공식 발표에서 확인할 수 있습니다.

앞서 언급한 주요 변경 사항 외에도 유니티는 유니버설 렌더 파이프라인의 셰이더 배리언트 생성 및 스트리핑을 개선하기 위해 계속 노력하고 있습니다. 또한 Unity의 셰이더 배리언트 관리 전반에 대한 추가적인 개선 사항도 검토 중입니다. 유니티의 최종 목표는 셰이더 빌드와 런타임 오버헤드를 최소화하면서 점점 늘어나는 기능을 원활하게 지원하는 것입니다.
현재 진행 중인 조사 중 일부는 유사한 배리언트 간 셰이더 리소스 중복 제거와 셰이더 키워드 및 셰이더 배리언트 컬렉션 API에 대한 전반적인 개선 사항과 관련이 있습니다. 개선 목표는 셰이더 배리언트 처리 및 런타임 성능과 관련하여 더 많은 유연성과 제어 기능을 제공하는 것입니다.
앞으로를 위해, 유니티는 에디터 내부에서 사용할 수 있는 셰이더 배리언트 추적 및 분석용 툴을 개발하여 셰이더 배리언트 사용에 관한 다음과 같은 세부 정보를 제공하고자 합니다.
- 가장 많은 배리언트를 생성하는 셰이더와 키워드
- 컴파일되었으나 런타임에 사용되지 않는 배리언트
- 제거되었으나 런타임에 필요한 배리언트
많은 분들의 피드백이 지금까지 가장 중요한 솔루션의 우선순위를 정하는 데 큰 도움이 되었습니다. 공개 로드맵을 확인하여 가장 적합한 기능에 투표해 주세요. 추가 변경 사항이 있으면 언제든지 기능 요청을 제출하거나 이 셰이더 포럼에서 팀에 직접 문의하세요.
