빌드 다섯 시간과 셰이더 0.5GB 는 같은 원인이다
Esc

    빌드 다섯 시간과 셰이더 0.5GB 는 같은 원인이다

    –Unity최적화렌더링

    셰이더 파일은 하나인데 빌드 결과물에는 여러 개가 들어갑니다. 키워드가 켜진 조합마다 별도의 컴파일 결과가 필요하기 때문입니다.

    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
    #pragma multi_compile _ _ADDITIONAL_LIGHTS
    #pragma multi_compile _ _SHADOWS_SOFT

    축이 셋이고 각각 켬과 끔이면 2 × 2 × 2 = 8개 입니다. 축이 열 개면 1,024개입니다. 그리고 축은 쉽게 열 개가 됩니다.

    곱셈이라는 게 요점입니다. 키워드 하나를 추가하는 것은 변형을 하나 늘리는 게 아니라 지금 있는 전부를 두 배로 만드는 일입니다.

    두 선언이 다르다

    multi_compile shader_feature
    빌드에 넣는 것 선언된 조합을 전부 머티리얼에서 실제로 켜진 것만
    런타임 키워드 토글 가능합니다 없는 조합이면 폴백합니다
    주 용도 파이프라인이 런타임에 전환하는 기능 아티스트가 인스펙터에서 켜고 끄는 기능
    변형 폭증 기여 큽니다 상대적으로 작습니다

    shader_feature 가 안전해 보이지만 함정이 있습니다. 미사용분을 지운다는 건 빌드 시점에 머티리얼을 스캔해서 판단한다는 뜻입니다.

    런타임에 EnableKeyword 로 켜려던 키워드를 shader_feature 로 선언해 두고 어떤 머티리얼도 그걸 켜 두지 않았다면, 그 변형은 빌드에 아예 없습니다. 그러면 폴백이 일어나고 화면이 이상해집니다. 에디터에서는 멀쩡합니다.

    문제가 네 방향이다

    상태 증상 대책
    너무 많다 클린 빌드 5시간 이상, 셰이더 하나에 런타임 메모리 0.5GB 스트리핑
    너무 적다 필요한 변형이 없어 다른 변형으로 조용히 폴백. 에디터와 다르게 보임 Strict Matching 으로 검출
    적정한데 미웜업 처음 그리는 순간 프레임 스파이크 웜업
    파악 자체가 불가능 대형 프로젝트에서 흔합니다 다이나믹 로딩

    “너무 적다” 가 있다는 게 중요합니다. 스트리핑은 많이 지울수록 좋은 게 아닙니다. 지우면 화면이 깨집니다.

    Strict Shader Variant Matching

    Project Settings 에 있는 옵션입니다. 켜면 폴백이 발생한 시점에 에러 로그를 뱉고 그 오브젝트를 핑크색으로 칠합니다.

    • 개발과 QA 빌드에서는 켭니다. 조용한 폴백이 눈에 보이는 사고가 됩니다
    • 출시 빌드에서는 끕니다. 사용자 화면에 핑크가 나오면 곤란합니다

    자동화된 플레이 테스트에 켜 두면 “이 씬에서 이 변형이 없다” 를 로그로 잡아 줍니다. 사람이 모든 씬을 돌아 볼 필요가 없어집니다.

    스트리핑

    빌드에 무엇을 넣을지 고르는 일이고, 도구가 둘인데 역할이 완전히 다릅니다.

    도구 누가 쓰나 단위
    ShaderKeywordFilter 렌더 파이프라인을 직접 만드는 사람 키워드 축을 통째로 제거
    IPreprocessShaders 애플리케이션 개발자, 즉 우리 변형 목록에서 개별 제거

    우리 쪽은 IPreprocessShaders 입니다. 빌드 콜백에서 IList<ShaderCompilerData> 를 받아서 안 쓸 것을 지웁니다.

    class StripVariants : IPreprocessShaders
    {
        public int callbackOrder => 0;
    
        public void OnProcessShader(Shader shader, ShaderSnippetData snippet,
                                    IList<ShaderCompilerData> data)
        {
            for (int i = data.Count - 1; i >= 0; i--)
            {
                // 2D 게임인데 3D 그림자 변형이 들어와 있다면 뺍니다
                if (data[i].shaderKeywordSet.IsEnabled(SoftShadows))
                    data.RemoveAt(i);
            }
        }
    }

    뒤에서부터 지우는 것만 조심하면 됩니다. 앞에서부터 지우면 인덱스가 밀립니다.

    지우기 전에 무엇이 실제로 쓰이는지부터 알아야 합니다. URP 에셋에 이미 있는 스트리핑 옵션을 먼저 확인하고, 그다음 빌드 로그의 변형 수를 보고, 그다음 코드로 갑니다. 아무 근거 없이 지우면 위의 “너무 적다” 로 갑니다.

    실제 효과

    전 후
    셰이더 하나만 쓰는 프로젝트의 셰이더 메모리 수십에서 수백 MB 10MB 이하
    새 빌드 머신에서의 클린 빌드 5시간 이상 1시간 미만

    두 번째 빌드가 빠른 건 착시입니다. 셰이더 컴파일 결과는 Library/ 아래에 캐시됩니다. 같은 머신에서 두 번째부터는 극적으로 빨라집니다. 진짜 빌드 시간은 새 CI 머신, 클린 클론, Library 삭제 후 의 시간입니다. 스트리핑의 가치를 재려면 거기서 재야 합니다.

    웜업과 다이나믹 로딩

    변형 수가 적정해도 처음 그리는 순간 프레임이 튑니다. GPU 프로그램을 그때 만들기 때문입니다. 프로파일러에 Shader.CreateGPUProgram 이나 Shader.CreateGPUPipeline 로 잡힙니다.

    이걸 로딩 화면에서 미리 처리하는 것이 웜업이고, 셰이더 데이터를 압축된 채로 들고 있다가 필요한 부분만 푸는 것이 다이나믹 로딩입니다.

    둘 다 그래픽스 API 에 따라 방법이 갈리고 (DX11 계열과 DX12/Metal/Vulkan 계열), 청크 크기와 캐시 개수를 따로 튜닝해야 합니다. 변형 수를 잡는 것과는 다른 작업입니다.

    정리

    • 변형은 키워드 축의 곱으로 늘어납니다. 축 하나를 더하면 개수가 두 배가 됩니다
    • multi_compile 은 선언한 조합을 전부 넣고, shader_feature 는 머티리얼이 켜 둔 것만 넣습니다
    • shader_feature 가 안전한 것은 아닙니다. 런타임에 EnableKeyword 로 켜려던 변형을 어떤 머티리얼도 안 켜 뒀으면 빌드에 없고, 조용히 폴백이 일어나 화면이 깨집니다
    • 방향이 넷입니다. 너무 많음(빌드 시간과 셰이더 용량) · 너무 적음(조용한 폴백) · 적정한데 미웜업(첫 렌더 스파이크) · 파악 불가(다이나믹 로딩)
    • Strict Shader Variant Matching 은 폴백을 핑크색으로 칠해 줍니다. 개발과 QA 빌드에서 켜고 출시 빌드에서 끕니다
    • 스트리핑은 IPreprocessShaders 쪽이 우리 몫입니다. ShaderKeywordFilter 는 파이프라인을 만드는 쪽 도구입니다
    • 두 번째 빌드가 빠른 건 착시입니다. Library/ 가 지워진 상태에서 재야 합니다