シェーダー基礎、UnityでShaderVariantを使ってコード分岐

シェーダー基礎、UnityでShaderVariantを使ってコード分岐

Unityシェーダーの基礎知識。
[KeywordEnum]を使ったShaderVariantの実装、シェーダーのコード分岐。

関連ページ 参考URL
Unityでは
ShaderVariant
という静的分岐の機能があります。
この機能を使えば、インスペクターからVariantを切り替えて、1つシェーダーに複数の効果を持たせることが出来ます。

※コードが古くなっていたのでURP用に記事を改修しました(2026/03/16)

ShaderVariantとは

まず今回テストに使うShaderVariantの効果を紹介します。
インスペクターからVariantを切り替えることで、「シルエット化無し/青シルエット化/赤シルエット化」切り替えています。

ShaderVariantは、端的に言うとC#のプリプロセッサに近い機能になります。
例えばC#ではプリプロセッサを使うことで、iOS端末とAndroid端末で処理を分けることが可能になります。
下はその一例です。
UNITY_IOS
UNITY_ANDROID
は、Unity側で標準に用意されているシンボル名です。
using UnityEngine;

public class TestPreprocessor : MonoBehaviour
{
    private void Start()
    {
//iOSビルド時はこっちの処理

#if UNITY_IOS
        Debug.Log("iOSの処理");
//Androidビルド時はこっちの処理

#elif UNITY_ANDROID
        Debug.Log("Androirの処理");
#endif
    }
}

C#はプロジェクトビルド時に、必要なプリプロセッサのコードのみビルドに含めます。
なので上の例だと、BuildSettingsがiOSの場合は
    private void Start()
    {
        Debug.Log("iOSの処理");
    }
というコードのみがビルドに含まれ、Androidの場合は
    private void Start()
    {
        Debug.Log("Androirの処理");
    }
というコードのみがビルドに含まれます。

ShaderVariantの概念はこのプリプロセッサとほぼ同じになります。
下はShaderVariantの重要な処理部分のみ抜粋したものです。
            half4 frag (Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

//_SILHOUETTE_NOのキーワードを選択している時は以下のコード

#if defined(_SILHOUETTE_NO)
                col.rgb *= input.color;
//_SILHOUETTE_BLUEのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_BLUE)
                col.rgb = half3(0, 0, 1);
//_SILHOUETTE_REDのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_RED)
                col.rgb = half3(1, 0, 0);
#endif
                return col;
            }

コード全文と解説

下が今回のテストのコード全文です。
Shader "UI/TestVariant"
{
    Properties 
    {
        
//インスペクターからVarinatを選択させるため設定

        [KeywordEnum(No, Blue, Red)] _SILHOUETTE("Silhouette Mode", Float) = 0
    }

    SubShader
    {
        Tags { "Queue" = "Transparent" }
        Cull Off
        ZWrite Off        
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            
//Varinatのキーワードを定義

            #pragma shader_feature _SILHOUETTE_NO _SILHOUETTE_BLUE _SILHOUETTE_RED

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"  
                        
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                half4 color : COLOR;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                half4 color : COLOR;
            };

            
//uGUI Image > Source Imageにアタッチされた画像を参照する

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            Varyings vert (Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                output.color = input.color;
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

//_SILHOUETTE_NOのキーワードを選択している時は以下のコード

#if defined(_SILHOUETTE_NO)
                col.rgb *= input.color;
//_SILHOUETTE_BLUEのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_BLUE)
                col.rgb = half3(0, 0, 1);
//_SILHOUETTE_REDのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_RED)
                col.rgb = half3(1, 0, 0);
#endif
                return col;
            }
            ENDHLSL
        }
    }
}

Variantに関係する部分だけ解説します。
インスペクター上にVariantの選択タブを表示させるために、まず下のコードを打ち込んでいます。
    Properties 
    {
        
//インスペクターからVarinatを選択させるため設定

        [KeywordEnum(No, Blue, Red)] _SILHOUETTE("Silhouette Mode", Float) = 0
    }
ここの部分は結構重要なので3つに分割して解説します。

[KeywordEnum(No, Blue, Red)]

ここに記述した文字列が実際に選択タブで表示されます。
文字列の指定に制限はなく何でも良いです。
ShaderVariantのプロパティ
ちなみにこの
[KeywordEnum]
マテリアルプロパティドロワー
、プロパティの中の()を
ドロワー引数
と呼びます。

_SILHOUETTE

この部分は制限がかなり厳しいです。
まず一番最初に_(アンダーバー)を付ける必要があります。
更に、後ろに出てくるVarinatのキーワードとリンクしている必要があります。
            
//Varinatのキーワードを定義

            #pragma shader_feature _SILHOUETTE_NO _SILHOUETTE_BLUE _SILHOUETTE_RED
今回は
_SILHOUETTE_NO
__SILHOUETTE_BLUE
_SILHOUETTE_RED
の3つのキーワードを定義しています。
なのでその共通する前半部分の
_SILHOUETTE
をプロパティにも入力しています。
もしこれが
_SILHOUETTE
ではなく
_SILHOUETTES
にした場合、キーワードと一致していないのでVariantが機能しません。

文字列が一致している必要があるものの、実はプロパティの方は小文字にするだけの変更なら問題ないです。 つまり下のコードでもOKです。
    Properties 
    {
        
//インスペクターからVarinatを選択させるため設定

        [KeywordEnum(No, Blue, Red)] _silhouette("Silhouette Mode", Float) = 0
    }

ただ問題なのは、実際のキーワードの定義の方は小文字が許されておらず、必ず全て大文字にする必要があります。
下の例ではVariantが動きません。
            
//Varinatのキーワードを定義

            #pragma shader_feature _silhouette_NO _silhouette_BLUE _silhouette_RED

わざわざプロパティだけ小文字にする意味もないので、今回のテストでは両方大文字を使っています。

("Silhouette Mode", Float) = 0

"Silhouette Mode"
はインスペクターに表示される文字列で、何を入力しても問題ないです。
第二引数に関しては、[KeywordEnum]で指定した変数には、0, 1, 2...という整数しか代入されないため、一見FloatよりIntの方が適切に思えます。
しかし
PropertiesにIntと入力しても結局処理的にはFloatとして扱われる
ため、わざわざIntと入力しても意味はないです。

= 0
がVariantの初期値の設定です。
もしこれが= 1であった場合、MaterialからTest/TetstVariantを指定した時の初期設定はBlueになります。

キーワード定義

ShaderVarinatのキーワードを定義する時、必ず
#pragma shader_feature
という記述が先に必要になります。
            
//Varinatのキーワードを定義

            #pragma shader_feature _SILHOUETTE_NO _SILHOUETTE_BLUE _SILHOUETTE_RED
今回の場合3つしか定義していないですが、別に4つでも5つでも良いです。
増やした分は、ちゃんとプロパティのKeywordEnumにも追記します。

フラグメントシェーダー

実際のシェーダー処理をしているフラグメントシェーダー部分は、今回の場合あまり特別なことをしていないです。
プリプロセッサの知識があればすっと入ってくるコードだと思います。

インスペクターから_SILHOUETTE_NOを選択していればシルエット化なし。
_SILHOUETTE_BLUEを選択していれば青シルエット化、_SILHOUETTE_REDを選択していれば赤シルエット化しています。
            half4 frag (Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

//_SILHOUETTE_NOのキーワードを選択している時は以下のコード

#if defined(_SILHOUETTE_NO)
                col.rgb *= input.color;
//_SILHOUETTE_BLUEのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_BLUE)
                col.rgb = half3(0, 0, 1);
//_SILHOUETTE_REDのキーワードを選択している時は以下のコード

#elif defined(_SILHOUETTE_RED)
                col.rgb = half3(1, 0, 0);
#endif
                return col;
            }

shader_featureとmulti_compile

本シェーダーではキーワードの定義にshader_featureを使っていますが、これをmulti_compileに置き換えても全く同様に動きます。
            
//Varinatのキーワードを定義

            
//#pragma shader_feature _SILHOUETTE_NO _SILHOUETTE_BLUE _SILHOUETTE_RED

            #pragma multi_compile _SILHOUETTE_NO _SILHOUETTE_BLUE _SILHOUETTE_RED
2つの処理の違いは、アプリビルド時にコードを軽量化するか否かです。
shader_featureはプロジェクト内のマテリアルで一つも指定されていないキーワードのコードを削ります。
対してmulti_compileは必ず全てビルドに含めます。

例えばプロジェクト内で
_SILHOUETTE_NO
のVariantのみ使っている場合は、
_SILHOUETTE_NO
の範囲のみがビルドに含まれます。
反対に
_SILHOUETTE_NO
のVariantのみ使っていない場合は、
_SILHOUETTE_BLUE
_SILHOUETTE_RED
の範囲のみがビルドに含まれます。
全て使っていれば全ての範囲が含まれます。

これだけ聞くとshader_featureの方が優秀に見えますが、一つ落とし穴があります。
マテリアルからそのキーワードを指定していなくても、C#のコードから動的に変更してそのキーワードに変更し使っている場合です。
この場合shader_featureを使うとエラーが出てしまいます。
0
0