무엇을 찾고 계신가요?
Engine & platform

스크립터블 셰이더 배리언트 제거

CHRISTOPHE RICCIO / UNITY TECHNOLOGIESContributor
May 14, 2018|12 분
스크립터블 셰이더 배리언트 제거
이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.

개발자가 Unity 셰이더 컴파일러에서 처리하고 플레이어 데이터에 포함할 셰이더 배리언트를 제어하여 플레이어 빌드 시간과 데이터 크기를 대폭 줄일 수 있습니다.

셰이더 배리언트의 수가 증가함에 따라 플레이어 빌드 시간과 데이터 크기는 프로젝트의 복잡성과 함께 증가합니다.

2018.2 베타 버전에 도입된 스크립터블 셰이더 배리언트 스트리핑을 사용하면 생성되는 셰이더 배리언트 수를 관리하여 플레이어 빌드 시간과 데이터 크기를 대폭 줄일 수 있습니다.

이 기능을 사용하면 유효하지 않은 코드 경로를 가진 모든 셰이더 배리언트를 제거하거나, 사용하지 않는 기능의 셰이더 배리언트를 제거하거나, 반복 시간이나 유지 관리 복잡성에 영향을 주지 않고 "디버그" 및 "릴리스"와 같은 셰이더 빌드 구성을 생성할 수 있습니다.

이 블로그 게시물에서는 먼저 당사가 사용하는 몇 가지 용어에 대해 정의합니다. 그런 다음 셰이더 배리언트의 정의에 집중하여 왜 그렇게 많은 배리언트를 생성할 수 있는지 설명합니다. 그 다음에는 자동 셰이더 배리언트 스트리핑에 대한 설명과 스크립터블 셰이더 배리언트 스트리핑이 Unity 셰이더 파이프라인 아키텍처에서 어떻게 구현되는지에 대한 설명이 이어집니다. 그런 다음 스크립트 가능한 셰이더 배리언트 스트리핑 API를 살펴본 후 파운틴블로 데모의 결과를 논의하고 스트리핑 스크립트 작성에 대한 몇 가지 팁으로 마무리합니다.

스크립터블 셰이더 배리언트 스트리핑을 배우는 것은 사소한 일이 아니지만 팀 효율성을 크게 높일 수 있습니다!

개념

스크립터블 셰이더 배리언트 스트리핑 기능을 이해하려면 관련된 다양한 개념을 정확하게 이해하는 것이 중요합니다.

  • 셰이더 에셋: 프로퍼티, 서브 셰이더, 패스, HLSL이 포함된 전체 파일 소스 코드입니다.
  • 셰이더 스니펫: 단일 셰이더 스테이지에 대한 종속성이 있는 HLSL 입력 코드입니다.
  • 셰이더 스테이지: GPU 렌더링 파이프라인의 특정 단계로, 일반적으로 버텍스 셰이더 단계와 프래그먼트 셰이더 단계입니다.
  • 셰이더 키워드: 셰이더의 컴파일 타임 브랜치에 대한 전처리기 식별자입니다.
  • 셰이더 키워드 세트: 특정 코드 경로를 식별하는 특정 셰이더 키워드 집합입니다.
  • 셰이더 배리언트: 특정 그래픽스 계층, 패스, 셰이더 키워드 세트 등에 대한 단일 셰이더 스테이지를 위해 Unity 셰이더 컴파일러에서 생성된 플랫폼별 셰이더 코드입니다.
  • Uber 셰이더: 다양한 셰이더 배리언트를 생성할 수 있는 셰이더 소스입니다.

Unity에서 우버 셰이더는 ShaderLab 서브 셰이더, 패스, 셰이더 유형과 #pragma multi_compile 및 #pragma shader_feature 전처리기 지시어로 관리됩니다.

생성된 셰이더 배리언트 수 세기

스크립터블 셰이더 배리언트 스트리핑을 사용하려면 셰이더 배리언트가 무엇인지, 셰이더 빌드 파이프라인에서 셰이더 배리언트가 어떻게 생성되는지 명확하게 이해해야 합니다. 생성되는 셰이더 배리언트의 수는 빌드 시간과 플레이어 셰이더 배리언트 데이터 크기에 정비례합니다. 셰이더 배리언트는 셰이더 빌드 파이프라인의 하나의 출력입니다.

셰이더 키워드는 셰이더 배리언트를 생성하는 요소 중 하나입니다. 셰이더 키워드를 신중하게 사용하지 않으면 셰이더 배리언트 수가 폭발적으로 증가하여 빌드 시간이 매우 길어질 수 있습니다.

셰이더 배리언트가 어떻게 생성되는지 확인하기 위해 다음 간단한 셰이더가 생성하는 셰이더 배리언트 수를 계산합니다:

Shader "ShaderVariantsStripping"
{
	SubShader
	{
		Pass
		{
			Name "ShaderVariantsStripping/Pass"

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY
			#pragma multi_compile OP_ADD OP_MUL OP_SUB

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}

			fixed4 get_color()
			{
				#if defined(COLOR_ORANGE)
					return fixed4(1.0, 0.5, 0.0, 1.0);
				#elif defined(COLOR_VIOLET)
					return fixed4(0.8, 0.2, 0.8, 1.0);
				#elif defined(COLOR_GREEN)
					return fixed4(0.5, 0.9, 0.3, 1.0);
				#elif defined(COLOR_GRAY)
					return fixed4(0.5, 0.9, 0.3, 1.0);
				#else
					#error "Unknown 'color' keyword"
				#endif
			}

			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 diffuse = tex2D(_MainTex, i.uv);

				fixed4 color = get_color();

				#if defined(OP_ADD)
					return diffuse + color;
				#elif defined(OP_MUL)
					return diffuse * color;
				#elif defined(OP_SUB)
					return diffuse - color;
				#else
					#error "Unknown 'op' keyword"
				#endif
			}
			ENDCG
		}
	}
}

프로젝트의 셰이더 배리언트 총 개수는 결정론적이며 다음 공식에 의해 주어집니다:

방정식

다음의 사소한 ShaderVariantStripping 예제를 통해 이 방정식을 명확하게 이해할 수 있습니다. 단일 셰이더로 다음과 같이 방정식을 단순화합니다:

방정식

마찬가지로 이 셰이더에는 단일 서브 셰이더와 단일 패스가 있어 방정식을 더욱 단순화합니다:

방정식

방정식의 키워드는 플랫폼 키워드와 셰이더 키워드를 모두 의미합니다. 그래픽 계층은 특정 플랫폼 키워드 세트 조합입니다.

ShaderVariantStripping/Pass에는 두 개의 멀티 컴파일 지시어가 있습니다. 첫 번째 지시어는 4개의 키워드(COLOR_ORANGE, COLOR_VIOLET, COLOR_GREEN, COLOR_GRAY)를 정의하고, 두 번째 지시어는 3개의 키워드(OP_ADD, OP_MUL, OP_SUB)를 정의합니다. 마지막으로 패스는 버텍스 셰이더 스테이지와 프래그먼트 셰이더 스테이지의 두 셰이더 스테이지를 정의합니다.

이 셰이더 배리언트 합계는 지원되는 단일 그래픽 API에 대해 제공됩니다. 하지만 프로젝트에서 지원되는그래픽 API에 대해 전용 셰이더 배리언트 세트가 필요합니다. 예를 들어 OpenGL ES 3과 Vulkan을 모두 지원하는 Android 플레이어를 빌드하는 경우 두 가지 셰이더 배리언트 세트가 필요합니다. 결과적으로 플레이어 빌드 시간과 셰이더 데이터 크기는 지원되는 그래픽 API의 수에 정비례합니다.

셰이더 빌드 파이프라인

Unity의 셰이더 컴파일 파이프라인은 프로젝트의 각 셰이더를 파싱하여 셰이더 스니펫을 추출한 후 multi_compile 및 shader_feature와 같은 변형 전처리 명령을 수집하는 블랙박스입니다. 그러면 셰이더 배리언트당 하나씩 컴파일 파라미터 목록이 생성됩니다.

이러한 컴파일 파라미터에는 셰이더 스니펫, 그래픽 계층, 셰이더 유형, 셰이더 키워드 세트, 패스 유형 및 이름이 포함됩니다. 설정된 각 컴파일 파라미터는 단일 셰이더 배리언트를 생성하는 데 사용됩니다.

따라서 Unity는 두 가지 휴리스틱을 기반으로 자동 셰이더 배리언트 스트리핑 패스를 실행합니다. 첫째, 스트리핑은 프로젝트 설정에 따라 이루어지며, 예를 들어 가상 현실 지원이 비활성화되어 있으면 VR 셰이더 배리언트가 체계적으로 스트리핑됩니다. 둘째, 자동 스트리핑은 그래픽 설정의 셰이더 스트리핑 섹션의 구성을 기반으로 합니다.

그래픽스 설정의 자동 셰이더 배리언트 스트리핑 옵션.
그래픽스 설정의 자동 셰이더 배리언트 스트리핑 옵션.

자동 셰이더 배리언트 스트리핑은 빌드 시간 제한을 기반으로 합니다. 셰이더 배리언트는 런타임 C# 실행에 따라 달라지므로 빌드 시점에 필요한 셰이더 배리언트만 자동으로 선택할 수 없습니다. 예를 들어 C# 스크립트가 포인트 라이트를 추가하지만 빌드 시점에 포인트 라이트가 없는 경우 셰이더 빌드 파이프라인이 플레이어에 포인트 라이트 셰이딩을 수행하는 셰이더 배리언트가 필요하다는 것을 파악할 방법이 없습니다.

다음은 자동으로 제거되는 키워드가 활성화된 셰이더 배리언트 목록입니다:

라이트맵 모드: LIGHTMAP_ON, DIRLIGHTMAP_COMBINED, DYNAMICLIGHTMAP_ON, LIGHTMAP_SHADOW_MIXING, SHADOWS_SHADOWMASK

안개 모드: FOG_LINEAR, FOG_EXP, FOG_EXP2

인스턴싱 배리언트: INSTANCING_ON

또한 가상 현실 지원이 비활성화되면 다음과 같은 활성화된 키워드가 내장된 셰이더 배리언트가 제거됩니다:

STEREO_INSTANCING_ON, STEREO_MULTIVIEW_ON, STEREO_CUBEMAP_RENDER_ON, UNITY_SINGLE_PASS_STEREO

자동 스트리핑이 완료되면 셰이더 빌드 파이프라인은 나머지 컴파일 파라미터 세트를 사용하여 셰이더 배리언트 컴파일을 병렬로 예약하여 플랫폼의 CPU 코어 스레드 수만큼 동시 컴파일을 시작합니다.

다음은 그 과정을 시각적으로 표현한 것입니다:

스크립트 가능한 셰이더 배리언트 스트리핑이 통합된 셰이더 파이프라인 아키텍처(주황색)입니다.
스크립트 가능한 셰이더 배리언트 스트리핑이 통합된 셰이더 파이프라인 아키텍처(주황색)입니다.

Unity 2018.2 베타에서는 셰이더 파이프라인 아키텍처에 셰이더 배리언트 컴파일 스케줄링 직전에 새로운 단계가 도입되어 사용자가 셰이더 배리언트 컴파일을 제어할 수 있습니다. 이 새로운 단계는 C# 콜백을 통해 사용자 코드에 노출되며, 각 콜백은 셰이더 스니펫별로 실행됩니다.

스크립터블 셰이더 배리언트 스트리핑 API

예를 들어, 다음 스크립트는 개발 플레이어 빌드에 사용되는 "DEBUG" 키워드로 식별되는 "DEBUG" 구성과 관련된 모든 셰이더 배리언트를 제거할 수 있습니다.

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

// Simple example of stripping of a debug build configuration
class ShaderDebugBuildProcessor : IPreprocessShaders
{
    ShaderKeyword m_KeywordDebug;

    public ShaderDebugBuildProcessor()
    {
        m_KeywordDebug = new ShaderKeyword("DEBUG");
    }

    // Multiple callback may be implemented.
    // The first one executed is the one where callbackOrder is returning the smallest number.
    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(
        Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> shaderCompilerData)
    {
        // In development, don't strip debug variants
        if (EditorUserBuildSettings.development)
            return;

        for (int i = 0; i < shaderCompilerData.Count; ++i)
        {
            if (shaderCompilerData[i].shaderKeywordSet.IsEnabled(m_KeywordDebug))
            {
                shaderCompilerData.RemoveAt(i);
                --i;
            }
        }
    }
}


셰이더 배리언트 컴파일 스케줄링 직전에 OnProcessShader가 호출됩니다.

셰이더, 셰이더스니펫데이터, 셰이더컴파일러데이터 인스턴스의 각 조합은 셰이더 컴파일러가 생성할 단일 셰이더 배리언트를 식별하는 식별자입니다. 셰이더 배리언트를 제거하려면 셰이더 컴파일러 데이터 목록에서 해당 셰이더 배리언트를 제거하기만 하면 됩니다.

셰이더 컴파일러가 생성해야 하는 모든 단일 셰이더 배리언트가 이 콜백에 나타납니다. 셰이더 배리언트 스트리핑 스크립트 작업을 할 때는 먼저 프로젝트에 유용하지 않으므로 제거해야 하는 배리언트를 파악해야 합니다.

결과
렌더 파이프라인을 위한 셰이더 배리언트 스트리핑

스크립터블 셰이더 배리언트 스트리핑의 한 가지 사용 사례는 셰이더 키워드의 다양한 조합으로 인해 렌더 파이프라인에서 유효하지 않은 셰이더 배리언트를 체계적으로 스트리핑하는 것입니다.

HD 렌더 파이프라인에 포함된 셰이더 배리언트 스트리핑 스크립트를 사용하면 HD 렌더 파이프라인을 사용하는 프로젝트의 빌드 시간과 크기를 체계적으로 줄일 수 있습니다. 이 스크립트는 다음 셰이더에 적용됩니다:

HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation

이 스크립트는 다음과 같은 결과를 생성합니다:

                                 Unstripped    Stripped 
Player Data Shader Variant Count 24350 (100%)  12122 (49.8%) 
Player Data Size on disk         511 MB        151 MB 
Player Build Time                4864 seconds  1356 seconds
퐁텐블로 포토그래메트리 데모의 스크린샷을 표준 PlayStation 4 1920x1080 해상도에서 HD 렌더 파이프라인을 사용하여 제작했습니다.
퐁텐블로 포토그래메트리 데모의 스크린샷을 표준 PlayStation 4 1920x1080 해상도에서 HD 렌더 파이프라인을 사용하여 제작했습니다.

또한 Unity 2018.2의 경량 렌더 파이프라인에는 셰이더 배리언트의 최대 98%를 자동으로 제거할 수 있는 스트리핑 스크립트를 자동으로 제공하는 UI가 포함되어 있어 모바일 프로젝트에 특히 유용할 것으로 기대됩니다.

프로젝트의 셰이더 배리언트 스트리핑

또 다른 사용 사례는 특정 프로젝트에 사용되지 않는 렌더 파이프라인의 모든 렌더링 기능을 제거하는 스크립트입니다. 라이트웨이트 렌더링 파이프라인에 대한 내부 테스트 데모를 사용하여 전체 프로젝트에 대해 다음과 같은 결과를 얻었습니다:

                                 Unstripped  Stripped 
Player Data Shader Variant Count 31080       7056 
Player Data Size on disk         121         116 
Player Build Time                839 seconds 286 seconds

보시다시피 스크립트 가능한 셰이더 배리언트 스트리핑을 사용하면 상당한 결과를 얻을 수 있으며, 스트리핑 스크립트에 대한 추가 작업을 통해 더 많은 것을 얻을 수 있습니다.

Lightweight 파이프라인 데모 스크린샷.
Lightweight 파이프라인 데모 스크린샷.
셰이더 배리언트 스트리핑 코드 작성에 대한 팁
셰이더 코드 디자인 개선

프로젝트에서 셰이더 배리언트 수가 폭발적으로 증가하여 컴파일 시간이 길어지고 플레이어 데이터 크기가 커질 수 있습니다. 스크립터블 셰이더 스트리핑은 이 문제를 해결하는 데 도움이 되지만 셰이더 키워드를 사용하여 보다 관련성 높은 셰이더 배리언트를 생성하는 방법을 다시 평가해야 합니다. 편집기에서 사용하지 않는 키워드를 테스트하기 위해 #pragma skip_variants를 사용할 수 있습니다.

예를 들어, 셰이더 스트리핑/컬러 셰이더에서 전처리 지시문은 다음 코드로 선언됩니다:

#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // color keywords
#pragma multi_compile OP_ADD OP_MUL OP_SUB // operator keywords


이 접근 방식은 색상 키워드와 연산자 키워드의 모든 조합이 생성됨을 의미합니다.

다음 장면을 렌더링한다고 가정해 보겠습니다:

COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.
COLOR_ORANGE + OP_ADD, COLOR_VIOLET + OP_MUL, COLOR_GREEN + OP_MUL.

첫째, 모든 키워드가 실제로 유용한지 확인해야 합니다. 이 장면에서는 COLOR_GRAY와 OP_SUB가 사용되지 않습니다. 이러한 키워드가 절대 사용되지 않는다는 것을 보장할 수 있다면 해당 키워드를 삭제해야 합니다.

둘째, 단일 코드 경로를 효과적으로 생성하는 키워드를 결합해야 합니다. 이 예에서 '추가' 작업은 항상 '주황색'으로만 사용됩니다. 따라서 이들을 하나의 키워드로 결합하고 아래와 같이 코드를 리팩터링할 수 있습니다.

#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN

#if defined(ADD_COLOR_ORANGE)
	#define COLOR_ORANGE
	#define OP_ADD
#elif defined(MUL_COLOR_VIOLET)
	#define COLOR_VIOLET
	#define OP_MUL
#elif defined(MUL_COLOR_GREEN)
	#define COLOR_GREEN
	#define OP_MUL
#endif


물론 키워드를 리팩터링하는 것이 항상 가능한 것은 아닙니다. 이러한 경우 스크립터블 셰이더 배리언트 스트리핑은 유용한 도구입니다!

callbackOrder를 사용하여 셰이더 배리언트를 여러 단계로 제거하기

각 스니펫에 대해 모든 셰이더 배리언트 스트리핑 스크립트가 실행됩니다. callbackOrder 멤버 함수가 반환하는 값에 순서를 지정하여 스크립트의 실행 순서를 지정할 수 있습니다. 셰이더 빌드 파이프라인은 콜백 순서가 높아지는 순서대로 콜백을 실행하므로 가장 낮은 것이 먼저, 가장 높은 것이 나중에 실행됩니다.

여러 셰이더 스트리핑 스크립트를 사용하는 사용 사례는 목적별로 스크립트를 분리하는 것입니다. 예를 들면 다음과 같습니다.

  • 스크립트 1: 잘못된 코드 경로를 가진 모든 셰이더 배리언트를 체계적으로 제거합니다.
  • 스크립트 2: 모든 디버그 셰이더 배리언트를 제거합니다.
변형
  • 스크립트 3: 현재 프로젝트에 필요하지 않은 코드 베이스의 모든 셰이더 배리언트를 제거합니다.
  • 스크립트 4: 스트리핑 스크립트의 빠른 반복 작업을 위해 나머지 셰이더 배리언트를 로깅하고 모두 스트리핑합니다.
셰이더 배리언트 스트리핑 스크립트 작성 프로세스

셰이더 배리언트 스트리핑은 매우 강력하지만 좋은 결과를 얻으려면 많은 작업이 필요합니다.

프로젝트 보기에서 모든 셰이더를 필터링합니다.

셰이더를 선택하고 인스펙터에서 표시를 클릭하여 해당 셰이더의 키워드/배리언트 목록을 엽니다. 빌드에 항상 포함되는 키워드 목록이 있습니다.

프로젝트에서 사용하는 특정 그래픽 기능을 알고 있어야 합니다.

모든 셰이더 단계에서 키워드가 사용되는지 확인합니다. 이러한 키워드를 사용하지 않는 스테이지에는 변형이 하나만 필요합니다.

스크립트에서 셰이더 배리언트를 스트립합니다.

빌드에서 비주얼을 확인합니다.

각 셰이더에 대해 2~6단계를 반복합니다.

샘플 프로젝트 다운로드

이 블로그 게시물을 설명하는 데 사용된 예제 프로젝트는 여기에서 다운로드할 수 있습니다. Unity 2018.2.0b1이 필요합니다.

유나이트 베를린에서 바이너리 배포 크기 최적화에 대해 자세히 알아보기

6월 21일 조나스 에크터호프의 강연에 참여하여 빌드의 최종 결과물을 더 잘 제어할 수 있는 새로운 툴에 대해 알아보세요!