[Unity] 描画モード切替に対応して、似たようなシェーダーを１つにまとめよう

これは[【unityプロ技】 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unityforpro)の10日目の記事です。

# はじめに

ひとつのシェーダーで複数の描画モード（例：ブレンドモード）に対応したいと思ったことはありませんか？

ブレンドモードには、不透明、加算合成、アルファブレンドなど様々な種類がありますが、
シェーダーを複製してブレンドモードだけを修正すると、似たようなシェーダーが乱立してしまい、実装コストも管理コストも増えてしまいます:sob:

本記事では、シェーダーのカスタムインスペクタを実装し、単一のシェーダーで様々な描画モードに切り替え可能にする方法をご紹介します。

# 完成したもの

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">描画モード切替に対応して、似たような <a href="https://twitter.com/hashtag/Unity3D?src=hash&amp;ref_src=twsrc%5Etfw">#Unity3D</a> シェーダーを1つにまとめる方法をQiitaに投稿しました！<br><br>カスタムインスペクタを用いて、ブレンドモード、カリングモード、ステンシル操作をマテリアルのプロパティとして選択可能にしています。<a href="https://t.co/OPxNSyzuvP">https://t.co/OPxNSyzuvP</a> <a href="https://t.co/7qsSAAvyRp">pic.twitter.com/7qsSAAvyRp</a></p>&mdash; がむ (@gam0022) <a href="https://twitter.com/gam0022/status/1204057405522231296?ref_src=twsrc%5Etfw">December 9, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 概要

基本的なしくみは非常にシンプルです。

- シェーダーに、BlendModeやZWriteのプロパティを用意する
- シェーダーのカスタムインスペクタで、BlendModeやZWriteをまとめて切り替えできるようにする

ちなみにUnityビルドインの[StandardShader](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Inspector/StandardShaderGUI.cs)でも同じような手法でブレンドモードの切り替えを実現しています。

StandardShaderは少々複雑ですので、今回はブレンドモードの切り替えに必要な部分だけ抽出して紹介します。

## 環境

- 2018.4.13f1 (LTS)
- Build-in Rendering Pipeline および LWRP（Light Weight Render Pipeline）

## ソースコード全文

ソースコード全文とプロジェクトファイルは[GitHub](https://github.com/gam0022/unity-multi-blend-mode-shader)に公開しています。

- [シェーダー: General.shader](https://github.com/gam0022/unity-multi-blend-mode-shader/blob/master/Assets/MultiBlendModeShader/Shaders/General.shader)
- [カスタムインスペクタ: GeneralShaderGUI.cs](https://github.com/gam0022/unity-multi-blend-mode-shader/blob/master/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs)

GitHubおよび本記事に登場するソースコードは[MIT License](https://github.com/gam0022/unity-multi-blend-mode-shader/blob/master/LICENSE)です。

# ソースコード解説

ここからは各処理について詳しく解説していきます。

## シェーダー: General.shader

### ブレンドモードのプロパティ化

まずはブレンドモード( `Blend` )と深度値の書き込みの有無（ `ZWrite` ）をプロパティ化します。

これらのプロパティはユーザには直接触らせず、カスタムインスペクタから自動設定させたいので、 `[HideInInspector]` アトリビュートを指定をしてインスペクタ上からは非表示にしています。

`_BlendMode` はカスタムインスペクタ用にブレンドモードの選択状態を保存するためのプロパティなので、Passの中では使いません。

```diff:General.shaderの先頭部分
 Shader "Unlit/General"
 {
     Properties
     {
+        // Blending state
+        [HideInInspector] _BlendMode ("__mode", Float) = 0.0
+        [HideInInspector] _SrcBlend ("__src", Float) = 1.0
+        [HideInInspector] _DstBlend ("__dst", Float) = 0.0
+        [HideInInspector] _ZWrite ("__zw", Float) = 1.0
 
         // Cutoutの閾値
         _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
 
         // メインテクスチャ
         _MainTex ("Texture", 2D) = "white" {}
 
         // 乗算カラー
         [HDR] _TintColor("Tint Color",  Color) = (1.0, 1.0, 1.0, 1.0)
     }
     SubShader
     {
         Tags { "RenderType"="Opaque" }
         LOD 100
 
         Pass
         {
+            Blend[_SrcBlend][_DstBlend]
+            ZWrite [_ZWrite]
             Cull Back
 
             CGPROGRAM
             #pragma vertex vert
             #pragma fragment frag
             // make fog work
             #pragma multi_compile_fog
```

### カスタムインスペクタの指定

`CustomEditor` でシェーダーのカスタムインスペクタを指定します。

```diff:General.shaderの末尾部分
    }
    Fallback "Unlit/Texture"
+   CustomEditor "GeneralShaderGUI"
 }
```

## カスタムインスペクタ: GeneralShaderGUI.cs

### ブレンドモードの定義

まず `BlendMode` というブレンドモードを定義するenumを定義します。

```cs
    /// <summary>
    /// ブレンドモード
    /// </summary>
    enum BlendMode
    {
        Opaque,
        Cutout,
        Transparent,
        Additive,
        Multiply,
    }
```

### ブレンドモードのポップアップの描画

`DrawBlendMode` メソッドでブレンドモードのポップアップの描画します。

シェーダーで定義した `_BlendMode` プロパティを `BlendMode` の enum に変換して、
`EditorGUILayout.Popup` でポップアップから選択できるようにします。

また、 `BlendMode.Cutout` のときだけに `_Cutoff` 用のインスペクタを表示します。

```cs
    /// <summary>
    /// BlendModeのインスペクタを描画します
    /// </summary>
    /// <param name="materialEditor">MaterialEditor</param>
    /// <param name="properties">Properties</param>
    void DrawBlendMode(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        var blendMode = FindProperty("_BlendMode", properties);
        var cutoff = FindProperty("_Cutoff", properties);
        var mode = (BlendMode) blendMode.floatValue;

        using (var scope = new EditorGUI.ChangeCheckScope())
        {
            mode = (BlendMode) EditorGUILayout.Popup("Blend Mode", (int) mode, Enum.GetNames(typeof(BlendMode)));

            if (scope.changed)
            {
                blendMode.floatValue = (float) mode;
                foreach (UnityEngine.Object obj in blendMode.targets)
                {
                    ApplyBlendMode(obj as Material, mode);
                }
            }
        }

        if (mode == BlendMode.Cutout)
        {
            materialEditor.ShaderProperty(cutoff, cutoff.displayName);
        }
    }
```

### ブレンドモードに応じて各種プロパティを設定

`ApplyBlendMode` メソッドが今回のカスタムインスペクタの **一番重要な部分** です。

選択されたブレンドモードに応じて各種プロパティやRenderType、renderQueueを適切に設定します。

```cs
    /// <summary>
    /// マテリアルのブレンドモードを変更します
    /// </summary>
    /// <param name="material">マテリアル</param>
    /// <param name="blendMode">ブレンドモード</param>
    static void ApplyBlendMode(Material material, BlendMode blendMode)
    {
        switch (blendMode)
        {
            case BlendMode.Opaque:
                material.SetOverrideTag("RenderType", "");
                material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.One);
                material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
                material.SetInt("_ZWrite", 1);
                material.DisableKeyword("_ALPHATEST_ON");
                material.renderQueue = -1;
                break;

            case BlendMode.Cutout:
                material.SetOverrideTag("RenderType", "TransparentCutout");
                material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.One);
                material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
                material.SetInt("_ZWrite", 1);
                material.EnableKeyword("_ALPHATEST_ON");
                material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.AlphaTest;
                break;

            case BlendMode.Transparent:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha);
                material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                material.SetInt("_ZWrite", 0);
                material.DisableKeyword("_ALPHATEST_ON");
                material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
                break;

            case BlendMode.Additive:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha);
                material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.One);
                material.SetInt("_ZWrite", 0);
                material.DisableKeyword("_ALPHATEST_ON");
                material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
                break;

            case BlendMode.Multiply:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.DstColor);
                material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
                material.SetInt("_ZWrite", 0);
                material.DisableKeyword("_ALPHATEST_ON");
                material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
                break;

            default:
                throw new ArgumentOutOfRangeException("blendMode", blendMode, null);
        }
    }
```

### シェーダー切替時にApplyBlendModeを呼ぶ

最後に `AssignNewShaderToMaterial` をオーバーライドをしてシェーダーのアサインの変更時に ApplyBlendMode を強制的に呼び出しています。

各種プロパティが常に正しい組み合わせになるようにするためのいわば安全装置です。

直前のシェーダーで各種プロパティが別の意味として使われていた場合にも、ApplyBlendMode を呼び出すことで正しい組み合わせにリセットできます。

```cs
    /// <summary>
    /// シェーダー切り替え時の処理をします
    /// </summary>
    /// <param name="material">マテリアル</param>
    /// <param name="oldShader">切り替え前のシェーダー</param>
    /// <param name="newShader">切り替え後のシェーダー</param>
    public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader)
    {
        base.AssignNewShaderToMaterial(material, oldShader, newShader);

        // MaterialのShader切り替え時にBlend指定が変更されてしまうので再設定します。
        ApplyBlendMode(material, (BlendMode) material.GetFloat("_BlendMode"));
    }
```

# 発展編

ここまでで描画モードの代表例としてブレンドモードの切り替え方法を紹介しましたが、カリングモードやステンシル操作といったその他の描画系の機能もプロパティ化するとより便利になります。

いずれもShaderLabの標準機能としてプロパティ化は想定されているので、実装は簡単です。

また、シェーダーバリアントのようにシェーダーを複数回コンパイルすることもないので、メモリやストレージ容量の増加などのデメリットもありません。

## カリングモードの切替

以下のようなコードでカリングモードもプロパティ化ができます（[GitHubのコミット](https://github.com/gam0022/unity-multi-blend-mode-shader/commit/431987703528d0efcc0512feb3339df76e20f457)）。

カリングモード専用の [`Rendering.CullMode`](https://docs.unity3d.com/ScriptReference/Rendering.CullMode.html) というEnumがUnityビルドインで定義されています。

```diff:General.shader
--- a/Assets/MultiBlendModeShader/Shaders/General.shader
+++ b/Assets/MultiBlendModeShader/Shaders/General.shader
@@ -11,6 +11,9 @@
         // Cutoutの閾値
         _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

+        // カリングモード
+        [Enum(UnityEngine.Rendering.CullMode)] _CullMode("Cull Mode", Float) = 2// Back
+
         // メインテクスチャ
         _MainTex ("Texture", 2D) = "white" {}

@@ -26,7 +29,7 @@
         {
             Blend[_SrcBlend][_DstBlend]
             ZWrite [_ZWrite]
-            Cull Back
+            Cull [_CullMode]

             CGPROGRAM
             #pragma vertex vert
```

```diff:GeneralShaderGUI.cs
--- a/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
+++ b/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
@@ -34,6 +34,9 @@ public class GeneralShaderGUI : ShaderGUI
         var tintColor = FindProperty("_TintColor", properties);
         materialEditor.ShaderProperty(tintColor, tintColor.displayName);

+        var cullMode = FindProperty("_CullMode", properties);
+        materialEditor.ShaderProperty(cullMode, cullMode.displayName);
+
         materialEditor.RenderQueueField();
     }
```

## ステンシル操作

同様にしてステンシル操作をプロパティ化できます（[GitHubのコミット](https://github.com/gam0022/unity-multi-blend-mode-shader/commit/7b8c8588597f964ba59cc0a5cd6efeb562a60aa7)）。

```diff:General.shader
--- a/Assets/MultiBlendModeShader/Shaders/General.shader
+++ b/Assets/MultiBlendModeShader/Shaders/General.shader
@@ -14,6 +14,13 @@
         // カリングモード
         [Enum(UnityEngine.Rendering.CullMode)] _CullMode("Cull Mode", Float) = 2// Back

+        // ステンシル
+        [Header(Stencil)]
+        _StencilRef("Stencil Ref", Range(0, 255)) = 0
+        [Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comp", Float) = 8// ALways
+        [Enum(UnityEngine.Rendering.StencilOp)] _StencilPassOp("Stencil Pass Op", Float) = 0// Keep
+        [Enum(UnityEngine.Rendering.StencilOp)] _StencilZFailOp("Stencil ZFail Op", Float) = 0// Keep
+
         // メインテクスチャ
         _MainTex ("Texture", 2D) = "white" {}

@@ -31,6 +38,14 @@
             ZWrite [_ZWrite]
             Cull [_CullMode]
 
+            Stencil
+            {
+                Ref [_StencilRef]
+                Comp [_StencilComp]
+                Pass [_StencilPassOp]
+                ZFail [_StencilZFailOp]
+            }
+
             CGPROGRAM
             #pragma vertex vert
             #pragma fragment frag
```

```diff:GeneralShaderGUI.cs
--- a/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
+++ b/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
@@ -37,6 +37,18 @@ public class GeneralShaderGUI : ShaderGUI
         var cullMode = FindProperty("_CullMode", properties);
         materialEditor.ShaderProperty(cullMode, cullMode.displayName);

+        var stencilRef = FindProperty("_StencilRef", properties);
+        materialEditor.ShaderProperty(stencilRef, stencilRef.displayName);
+
+        var stencilComp = FindProperty("_StencilComp", properties);
+        materialEditor.ShaderProperty(stencilComp, stencilComp.displayName);
+
+        var stencilPassOp = FindProperty("_StencilPassOp", properties);
+        materialEditor.ShaderProperty(stencilPassOp, stencilPassOp.displayName);
+
+        var stencilZFailOp = FindProperty("_StencilZFailOp", properties);
+        materialEditor.ShaderProperty(stencilZFailOp, stencilZFailOp.displayName);
+
         materialEditor.RenderQueueField();
     }
```

# まとめ

シェーダーのカスタムインペクタを実装することで、自作シェーダーでもStandardShaderのようにブレンドモードを切替可能にする方法をご紹介しました。

さらに、発展編として、カリングモードとステンシル操作もプロパティ化もしました。

ブレンドモードやカリングモード、ステンシル操作といった各種描画モードごとにシェーダーを実装しなくて済むため、工数削減のメリットがあります。

実際のゲームのプロジェクト開発においても

- 描画設定をなるべくプロパティ化した汎用シェーダー（いわゆる Uber Shader）
- 特定の演出やモデルに特化した専用シェーダー

というようにシェーダーの役割を2つに分けることで、似たようなシェーダーの乱立を防ぎ、管理工数や実装工数を大きく削減できます。

また、汎用シェーダーを定義することで、プロパティ設定だけで幅広い描画設定に対応できるので、プログラマーのシェーダー修正なしにデザイナーだけで作業を進められるというメリットもあります。

みなさんも参考にしてみてください！

# 関連記事

- [エフェクト統一シェーダーがあると捗るよね - SEGA TECH Blog](http://techblog.sega.jp/entry/2017/10/25/100000)
- [【Unity】【シェーダ】【エディタ拡張】BlendModeをインスペクタから指定する - LIGHT11](http://light11.hatenadiary.com/entry/2019/01/14/235700)

<!--
## テクスチャ素材

画像素材の著作権は私 @gam0022 にあります。

- アイコン画像はInkscapeで自作しました。
- Skyboxは自作のシェーダーでプロシージャル生成しました。 [WORMHOLE](https://gam0022.net/blog/2018/12/12/tdf2018/)というUnityによる映像作品のために開発したものを流用しました。
-->