URP shader toggle by multi_compile

multi_compile도 지시어도 shader_feature 지시어 처럼 토글(체크박스)로 셰이더의 기능을 On/Off 할수 있습니다.

두 지시어의 차이점에 대해 간단하게 알아 보겠습니다.

shader_feature vs multi_compile:

shader_feature:
  • 메테리얼에서 쓰지 않는 변종은 빌드 시 포함하지 않습니다. (용량 절약, 보통 재질 설정에 사용)
  • 용도 : 메테리얼 설정 (개별 기능 On/Off)
  • 빌드 최적화 : 사용 중인 조합만 빌드에 포함       
  • 런타임 제어 : 어려움 (빌드 시 빠질 수 있음)

multi_compile:
  • 사용 여부와 관계없이 모든 변종을 빌드합니다. (주로 그림자, 안개 등 시스템 설정에 사용)
  • 용도 : 전역(Global) 설정 (안개, 그림자 등)
  • 빌드 최적화 : 모든 가능한 조합을 빌드에 포함
  • 런타임 제어 : 모두 포함, 자유로움 (언제든 끄고 켜기 가능)
런타임에 코드로 자유롭게 끄고 켜야 하는 기능이라면 shader_feature 대신 multi_compile을 사용하는 것이 안전합니다.

다음 예제는 multi_compile을 이용하여 텍스쳐를 적용 할지, 안할지 토글하여 선택 할수 있습니다.
셰이더를 초기화 상태로 되돌리려면 Reset을 해야 합니다.

실질적으로는 shader_feature 키워드에서 multi_compile로 키워드 한줄만 바뀌었습니다.

1. 퍼로퍼티 추가

[Toggle] 속성을 사용하여 인스펙터에 체크박스를 생성합니다.
괄호 안의 _USE_TEX_ON은 실제 셰이더 키워드 이름이 됩니다.

    Properties
    {
        [Toggle(_USE_TEX_ON)] _UseTex ("Use Texture?", Float) = 0.0
        _BaseMap("BaseMap", 2D) = "white" {}
        _BaseColor("Base Color (Fallback)", Color) = (1, 1, 1, 1)
    }

2. 셰이더 키워드 선언 (텍스처 사용 여부 분기)

    Pass
    {
        HLSLPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #pragma multi_compile _ _USE_TEX_ON

multi_compile의 장점과 단점

  • 장점: 게임 실행 중에 언제든지 EnableKeyword를 통해 기능을 켜도 즉시 대응할 수 있습니다. 셰이더가 빌드에서 누락되어 화면이 핑크색으로 변할 걱정이 없습니다.
  • 단점: 키워드가 많아지면 빌드 용량이 커지고 셰이더 컴파일 시간이 길어집니다.


3. 체크박스가 켜져 있을 때만 텍스처를 샘플링합니다.


half3 frag(Varyings IN) : SV_Target
{
    ......

    #if defined(_USE_TEX_ON)
      albedo *= SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, TRANSFORM_TEX(IN.uv, _BaseMap)).rgb;
    #endif

    return albedo * (directLight + ambient);
}

주의할 점: 셰이더 변종 폭발 (Variant Explosion)

multi_compile을 남발하면 안 되는 이유입니다.

만약 다음과 같이 선언하면 어떻게 될까요?
  • #pragma multi_compile A B (2개)
  • #pragma multi_compile C D (2개)
  • #pragma multi_compile E F (2개)

유니티는 이들의 모든 조합인 2 X 2 X 2 = 8개의 셰이더를 만듭니다.
키워드가 10개만 되어도 수천 개의 셰이더가 생성되어 빌드 시간이 몇 시간씩 걸릴 수 있습니다.

요약
  • multi_compile은 어떤 상황에서도 해당 기능이 작동하도록 모든 셰이더 버전을 빌드에 포함시키는 지시어입니다.
  • 런타임에 스크립트로 기능을 끄고 켜야 한다면 multi_compile이 정답입니다.
  • 반면, 메테리얼 설정으로 고정해서 쓸 기능이라면 shader_feature가 최적화에 유리합니다

MyUnlitBasic_multi_cimpile.shader 파일
Shader "Custom/UnlitShaderBasic_MultiCompile"
{
    Properties
    {
        // 인스펙터 토글. 런타임에 EnableKeyword/DisableKeyword로 제어 가능합니다.
        [Toggle(_USE_TEX_ON)] _UseTex ("Use Texture?", Float) = 0.0
        _BaseMap("BaseMap", 2D) = "white" {}
        _BaseColor("Base Color (Fallback)", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // shader_feature 대신 multi_compile을 사용하여
            // 빌드 시 모든 경우의 수(On/Off)를 포함시킵니다.
            // '_'는 키워드가 없는 상태(Off)를 의미합니다.
            #pragma multi_compile _ _USE_TEX_ON

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS   : POSITION;
                float2 uv            : TEXCOORD0;
                float3 normalOS       : NORMAL; 
            };

            struct Varyings
            {
                float4 positionHCS  : SV_POSITION;
                float2 uv            : TEXCOORD0;
                float3 normalWS       : NORMAL;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = IN.uv;
                OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS); 
                return OUT;
            }

            half3 frag(Varyings IN) : SV_Target
            {
                Light light = GetMainLight();
                float3 lightDir = normalize(light.direction); 
                float3 normalWS = normalize(IN.normalWS);
 
                float NdotL = saturate(dot(normalWS, lightDir));
                float3 directLight = NdotL * light.color;
                half3 ambient = unity_AmbientSky.rgb;

                // 기본 색상 설정
                half3 albedo = _BaseColor.rgb;

                // multi_compile로 생성된 키워드 분기
                #if defined(_USE_TEX_ON)
                    albedo *= SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, TRANSFORM_TEX(IN.uv, _BaseMap)).rgb;
                #endif

                return albedo * (directLight + ambient);
            }
            ENDHLSL
        }
    }
}

"Use Texture"를 토글 하여, 텍스쳐를 적용 할수 있습니다.

4. 셰이더 최종 정리

multi_compile _ _USE_TEX_ON 처럼 언더바는 항상 포함 시켜 줍니다.

체크의 초기 값은   다음과 같이 1.0과 0.0으로 해주는게 가독성에 좋습니다.
  • [Toggle(_USE_TEX_ON)] _UseTex ("Use Texture?", Float) = 1.0
  • [Toggle(_USE_TEX_ON)] _UseTex ("Use Texture?", Float) = 0.0

Properties
{
    // 1.0이면 생성 시 자동 체크, 0.0이면 체크 해제 상태
    [Toggle(_USE_TEX_ON)] _UseTex ("Use Texture?", Float) = 1.0
    _BaseMap("BaseMap", 2D) = "white" {}
}

// ...

HLSLPROGRAM
// _ 는 '아무것도 없음(Off)', _USE_TEX_ON은 '켜짐(On)'
#pragma multi_compile _ _USE_TEX_ON

// ...

#if defined(_USE_TEX_ON)
    // 텍스처 연산 수행
#endif

5. C#에서 제어 하는 방법

_USE_TEX_ON로 활성화, 비활성화 하면 됩니다.

using UnityEngine;

public class TextureToggleController : MonoBehaviour
{
    private Material targetMat;
    private bool isTexOn = true;

    void Start()
    {
        // 렌더러에서 메테리얼 인스턴스를 가져옵니다.
        targetMat = GetComponent<Renderer>().material;
    }

    void Update()
    {
        // 스페이스바를 누를 때마다 토글
        if (Input.GetKeyDown(KeyCode.Space))
        {
            isTexOn = !isTexOn;

            if (isTexOn)
                targetMat.EnableKeyword("_USE_TEX_ON");
            else
                targetMat.DisableKeyword("_USE_TEX_ON");
               
            Debug.Log($"Texture is now: {(isTexOn ? "ON" : "OFF")}");
        }
    }
}