渋谷ほととぎす通信

エンジニア社長によるUnityとAIのブログ & エンジニアの生存戦略

【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門

【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門

こんにちは、エンジニアのオオバです。

お悩みさん
お悩みさん
  • シェーダーのマクロって何?
  • `multi_compile`がよくわからない
  • `shader_feature`とは?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    シェーダーにはマクロと呼ばれる機能が実装されています。マクロを使うとシェーダー内のコードの置き換えや条件分岐を簡単に実装することが可能です。

    またシェーダーには「multi_compile」と「shader_feature」と呼ばれる 異なるバリエーション(分岐)シェーダーを実行時に切り替える機能 です。この分岐したシェーダーの事をシェーダーバリアントと呼びます。

    マクロ、multi_compileshader_featureは密接な関係です。これらを使いこなすことでシェーダーを書く際の選択肢が大きく広がります。まだ使っていない方はこの記事を通してぜひ覚えてしまいましょう。

    👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

    シェーダーのマクロとは?

    Unityのシェーダーで使える「マクロ」とは何でしょうか。マクロとはコードの一部を置き換えられるプリプロセッサです。プリプロセッサマクロとも呼ばれますが、本記事では単に「マクロ」と呼称します。

    ちなみに「プリプロセッサ」とはコンパイル前に処理挟める仕組みのこと。つまりマクロは シェーダーのコンパイル前にコードの一部を置き換えることができる機能 ということです。

    言葉だけでは分かりづらいためソースコードでマクロを確認してみましょう。

    #define HOGE(a) fixed4(a, 0, 0, 1);
    
    fixed4 frag (v2f i) : SV_Target  
    {
        return HOGE(1);  
    }
    

    上記はシェーダーコードの一部で、マクロを適用したフラグメントシェーダーです。 #define マクロ名(引数) 処理 という文法でマクロを記述できます。

    マクロ「HOGE」はfixed4型を返却する関数のような処理になっていることが分かります。

    HOGE(1) と記述したということはコンパイル時には fixed4(1, 0, 0, 1) と展開されるということです。

    【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門_0

    このシェーダーを当てると上図のように赤色に描画されます。

    このようにマクロを使うことで簡単にコードの置き換えられます。

    またマクロ名は一般的にアッパースネークケース(大文字 + アンダースコア)で記述するので覚えておきましょう。

    ※小文字を使ってもマクロは動作します

    複数行に渡るマクロの記述方法

    マクロの処理が長くなると改行したくなります。以下のように記述するとコンパイルエラーです。

    #define TEST_MACRO(abc)
        abc.x = 0;  
    

    こちらのように\ (バックスラッシュ)をお尻に付けるとコンパイルが通ります。

    #define TEST_MACRO(abc) \
        abc.x = 0;  
    

    書きなれないかもしれませんが、バックスラッシュを使って改行処理を記述していきましょう。

    multi_compileとshader_featureとは?

    次に「multi_compile」と「shader_feature」について解説します。

    繰り返しになりますが、multi_compileshader_featureはビルド時にコンパイルされたシェーダーバリエーション(シェーダーバリアント)を 実行時に切り替える機能 です。具体的にどのようにシェーダーバリアントを作るのでしょうか。

    次のコードを見てみましょう。

    fixed4 frag (v2f i) : SV_Target  
    {
        // デフォルト色(緑)  
        fixed4 color = fixed4(0, 1, 0, 1);  
    
        #ifdef YELLOW  
            color = fixed4(1, 1, 0, 1);  
        #elif defined(BLUE)  
            color = fixed4(0, 0, 1, 1);  
        #endif  
    
        return color;  
    }
    

    #ifdef〜#elif〜#endifを使って処理を分岐しました。マクロ「YELLOW」が有効なら黄色、「BLUE」が有効なら青色に描画されます。

    【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門_1

    現状、YELLOWもBLUEも有効化されていないため、デフォルト色の緑が描画されます。

    では次にマクロを有効化してみましょう。マクロを有効化する方法は2種類あります。

    1.defineを使ったマクロ切り替え

    1つ目は #define を使用したマクロ切り替えです。次のコードを見てみましょう。

    #define YELLOW
    
    fixed4 frag (v2f i) : SV_Target  
    {
        // デフォルト色(緑)  
        fixed4 color = fixed4(0, 1, 0, 1);  
    
        #ifdef YELLOW  
            color = fixed4(1, 1, 0, 1);  
        #elif defined(BLUE)  
            color = fixed4(0, 0, 1, 1);  
        #endif  
    
        return color;  
    }
    

    マクロ「YELLOW」を有効化しました。文法は #define マクロ名 で有効化します。

    【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門_2
    YELLOWを有効化したことで有効化したことでシェーダーは黄色を描画しました。

    2.C#からマクロ切り替え

    もう1つはC#コードからマクロの切り替えです。Materialに対して EnableKeyword 関数を実行することでマクロを有効化します。

    [SerializeField] private Renderer _renderer;  
    
    void Awake()  
    {
        _renderer.material.EnableKeyword("YELLOW");  
    }
    

    上記のように EnableKeyword でマクロYELLOWを有効化できます。

    しかしまだ準備不足で上記の コードは正しく動作しません。 ここでmulti_compileshader_featureが登場します。

    multi_compileとshader_featureでコンパイル指定

    multi_compileshader_featureを使用することでシェーダーコードのコンパイル指示ができます。

    つまり EnableKeyword("YELLOW"); が動作しない理由は マクロ「YELLOW」が有効なバリエーションがコンパイルされなかったため です。

    そこで multi_compileshader_featureの登場です。まずはmulti_compileを使った例を見ていきましょう。

    #pragma multi_compile _ YELLOW
    
    fixed4 frag (v2f i) : SV_Target  
    {
        fixed4 color = fixed4(0, 1, 0, 1);  
    
        #ifdef YELLOW  
            color = fixed4(1, 1, 0, 1);  
        #elif defined(BLUE)  
            color = fixed4(0, 0, 1, 1);  
        #endif  
    
        return color;  
    }
    

    ポイントはここ#pragma multi_compile _ YELLOW です。

    文法はこちら #pragma multi_compile マクロ名 。今回の場合は「_(アンダースコア)」と「YELLOW」をコンパイルするように指示しています。

    【Unity】シェーダーのマルチコンパイルとシェーダーフィーチャー入門_3
    YELLOWがコンパイルされたことで黄色が描画されるようになりました。

    お悩みさん
    お悩みさん
    「_(アンダースコア)」をコンパイル対象にしているのはなぜ?

    multi_compileに「_(アンダースコア)」を指定している理由は、YELLOWが存在しない場合用です。もし下記のように「_」を除いてYELLOWだけを指定したらどうなるでしょうか。

    #pragma multi_compile YELLOW
    

    YELLOWの存在しないシェーダーがコンパイルされないためマクロ「YELLOW」が常に有効化されてしまうということです。 指定したマクロが有効ではない場合も存在するなら「_」をコンパイル対象に含めましょう。

    次にshader_featureの例を見ていきましょう。基本的にmulti_compileと記述方法は同じです。

    #pragma shader_feature _ YELLOW
    
    fixed4 frag (v2f i) : SV_Target  
    {
        fixed4 color = fixed4(0, 1, 0, 1);  
    
        #ifdef YELLOW  
            color = fixed4(1, 1, 0, 1);  
        #elif defined(BLUE)  
            color = fixed4(0, 0, 1, 1);  
        #endif  
    
        return color;  
    }
    

    #pragma shader_feature _ YELLOW と記述することでYELLOマクロのある場合とない場合をコンパイルします。

    multi_compileとshader_featureの違いって何?

    multi_compileshader_featureの違いはシェーダーバリアントのコンパイル対象です。multi_compileは指示されたシェーダーバリアントを全てコンパイル対象にします。

    一方shader_featureは未使用のマクロをコンパイルから外します。ここで言う「マクロの使用」とは #defineMaterial.EnableKeyword を指します。

    つまりmulti_compileは全部入り、shader_featureは使用した分だけシェーダーバリアントが作られるということです。shader_featureの方が無駄を省く分ビルドサイズが節約できるかもしれません。

    まとめ

    本記事ではマクロ、multi_compileshader_featureの入門コンテンツとして解説してきました。

    簡単に記事の内容をまとめます。

    マクロ・multi_compile・shader_featureまとめ

    ①マクロとはコンパイル前のコード置き換え

    ②multi_compile、shader_featureはシェーダーバリアントのコンパイル指示

    ③shader_featureは未使用マクロを除外してコンパイル

    マクロ・multi_compileshader_featureを使うことでシェーダー内の分岐を簡単に実装することができます。

    シェーダーってどうしてもいろんなパターンを網羅する必要が出てくるため、適切にバリアント化する必要が出てきます。

    これからシェーダーを始める方はぜひともマクロ・multi_compileshader_featureを正しく理解して使っていただければと思います。

    オススメ記事