Help us understand the problem. What is going on with this article?

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

これは【unityプロ技】 Advent Calendar 2019の10日目の記事です。

はじめに

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

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

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

完成したもの

概要

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

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

ちなみにUnityビルドインのStandardShaderでも同じような手法でブレンドモードの切り替えを実現しています。

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

環境

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

ソースコード全文

ソースコード全文とプロジェクトファイルはGitHubに公開しています。

GitHubおよび本記事に登場するソースコードはMIT Licenseです。

ソースコード解説

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

シェーダー: General.shader

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

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

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

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

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 でシェーダーのカスタムインスペクタを指定します。

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

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

ブレンドモードの定義

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

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

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

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

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

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

    /// <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を適切に設定します。

    /// <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 を呼び出すことで正しい組み合わせにリセットできます。

    /// <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のコミット)。

カリングモード専用の Rendering.CullMode というEnumがUnityビルドインで定義されています。

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
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のコミット)。

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
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つに分けることで、似たようなシェーダーの乱立を防ぎ、管理工数や実装工数を大きく削減できます。

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

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

関連記事

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした