-100p

-10p

+10p

+100p

シェーダー基礎、UnityでShaderVariant

Unityシェーダーの基礎知識。
ShaderVariantを使って複数の効果を1つのシェーダーに実装。

関連ページ 参考URL
UnityではShaderVariantという機能がある。
この機能を使えば、インスペクターからVariantを切り替えて、1つシェーダーに複数の効果を持たせることが出来る。
とても便利だが実質if文の亜流のような感じなので、使い方によっては処理時間が伸びる可能性もある。(Shaderではif文が推奨されていない)

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の重要な処理部分のみ抜粋したもの。
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
//_COLOR_REVERSALのシンボル名を選択している時は以下のコード

#ifdef _COLOR_REVERSAL
                col.r = 1 - col.r;
                col.g = 1 - col.g;
                col.b = 1 - col.b;
//_COLOR_SILHOUETTEのシンボル名を選択している時は以下のコード

#elif _COLOR_SILHOUETTE
                col.r = i.color.r;
                col.g = i.color.g;
                col.b = i.color.b;
#endif
                return col;
            }
プロジェクト内で
 _COLOR_REVERSAL 
のVariantのみ使っている場合は、
 _COLOR_REVERSAL 
の範囲のみがビルドに含まれる。
反対に
 _COLOR_SILHOUETTE 
のVariantのみ使っている場合は、
 _COLOR_SILHOUETTE 
の範囲のみがビルドに含まれる。

問題はプロジェクト内で両方のVariantを使ってる場合で、この場合Unityは両方のコードをビルドに含める。
こうなると、実質if文を使ってるのと変わらないので、シェーダーの処理速度の劣化に繋がる。

コード全文と解説

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

        [KeywordEnum(Reversal, Silhouette)] _COLOR("Color", Int) = 0
    }

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
//Varinatのシンボル名を定義

            #pragma multi_compile _COLOR_REVERSAL _COLOR_SILHOUETTE

            struct appdata
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };

            sampler2D _MainTex;

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

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
//_COLOR_REVERSALのシンボル名を選択している時は以下のコード

#ifdef _COLOR_REVERSAL
                col.r = 1 - col.r;
                col.g = 1 - col.g;
                col.b = 1 - col.b;
//_COLOR_SILHOUETTEのシンボル名を選択している時は以下のコード

#elif _COLOR_SILHOUETTE
                col.r = i.color.r;
                col.g = i.color.g;
                col.b = i.color.b;
#endif
                return col;
            }
            ENDCG
        }
    }
}

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

        [KeywordEnum(Reversal, Silhouette)] _COLOR("Color", Int) = 0
    }
ここの部分は結構重要なので3つに分割して解説。

[KeywordEnum(Reversal, Silhouette)]

ここに記述した文字列が実際に選択タブで表示される。
文字列の指定に制限はなく何でも良い。

_COLOR

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

            #pragma multi_compile _COLOR_REVERSAL _COLOR_SILHOUETTE
今回は
 _COLOR_REVERSAL 
 _COLOR_SILHOUETTE 
の2つのシンボル名を定義している。
なのでその共通する前半部分の
 _COLOR 
をプロパティにも入力している。
もしこれが
 _COLOR 
ではなく
 _COLORS 
にした場合、シンボル名と一致していないのでVariantが機能しない。

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

        [KeywordEnum(Reversal, Silhouette)] _color("Color", Int) = 0
    }

ただ問題なのは、実際のシンボル名の定義の方は小文字が許されておらず、必ず全て大文字にする必要がある。
下の例ではVariantが動かない。
            
//Varinatのシンボル名を定義

            #pragma multi_compile _color_REVERSAL _color_SILHOUETTE

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

("Color", Int) = 0

 "Color" 
はインスペクターに表示される文字列、何を入力しても問題ない。
第二引数の
 Int 
に関しては、色々試したがFloatでも問題ない模様。
ただVariantのプロパティに0.4や0.6のfloat値を打ち込んでも結局0扱いされるので、Intで良いと思う。

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

シンボル名定義

ShaderVarinatのシンボル名を定義する時、必ず
 #pragma multi_compile 
という記述が先に必要になる。
            
//Varinatのシンボル名を定義

            #pragma multi_compile _COLOR_REVERSAL _COLOR_SILHOUETTE
今回の場合2つしか定義していないが、別に3つでも4つでも良い。
増やした分は、ちゃんとプロパティのKeywordEnumにも追記する。

フラグメントシェーダー

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

インスペクターからReversalを選択していれば色相反転処理を、Silhoutteを選択していればシルエット化処理をしている。
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
//_COLOR_REVERSALのシンボル名を選択している時は以下のコード

#ifdef _COLOR_REVERSAL
                col.r = 1 - col.r;
                col.g = 1 - col.g;
                col.b = 1 - col.b;
//_COLOR_SILHOUETTEのシンボル名を選択している時は以下のコード

#elif _COLOR_SILHOUETTE
                col.r = i.color.r;
                col.g = i.color.g;
                col.b = i.color.b;
#endif
                return col;
            }
0
0

-100p

-10p

+10p

+100p