【Unity】加算や減算などRenderingModeの切り替えをやりやすくするCustomShaderGUIのサンプル作ってみた

仕事でUnityのシェーダーをちょくちょくいじってるのですが、

不透明用シェーダー・アルファブレンドシェーダー・加算用シェーダー・減算用シェーダー…という風に

これらを別シェーダーファイルで用意しちゃうと管理が厄介ですよね。

 

かといって、ブレンドの変更をする場合にただ単にSrcBlend と DstBlend を

One や SrcAlpha や OneMinusSrcAlpha などから選択できるようにしてもどの組み合わせにしたら加算になるんやっけ?

と使ってもらうデザイナーが混乱するかもしれません。

 

なのでStandardShaderについてるRenderingModeみたいにできたら分かりやすいなと思い、

CustomShaderGUIを調べて公式のビルトインシェーダーの中に入っているStandardShaderGUI.csを参考に

それを自分なりに噛み砕いてもう少しシンプルにしたものを作ってみました。

 

Unityで確認されたい場合は下の方にシェーダー2つとCustomShaderGUI用のC#スクリプトのコードを書いてますので

それをコピーしてもらうか、こちらからDLしてもらって、

  ・shaderファイルはShaderフォルダなど適当な場所

  ・csファイルはScriptsフォルダなどの中のEditorフォルダ

に置いてください。

まずはGUIのスクリプトの前に、今回のサンプルに使用するシェーダーの説明です。

 

内容は簡単なもので、1つ目はテクスチャカラーとカラープロパティーを乗算しただけのシェーダー、

2つ目は1つ目のシェーダーのパスをUsePassで使いつつ、別パスでOutlineを描画しているシェーダーです。

 

1つ目のシェーダーコードです。

Shader "Custom/TestShader"
{
    Properties
    {
        _MainTex("Main Tex"2D) = "white" {}
        _Color("Color"Color) = (1.01.01.01.0)

        _Cutoff("Alpha Cutoff"Range(0.01.0)) = 0.5

        [KeywordEnum(OffFrontBack)] _Cull ("Culling"Float) = 2 

        [HideInInspector] _Mode ("__mode"Float) = 0.0
        [HideInInspector] _SrcBlend ("__src"Float) = 1.0
        [HideInInspector] _DstBlend ("__dst"Float) = 0.0
        [Toggle] _ZWrite ("__zw"Float) = 1.0
        [HideInInspector][Toggle] _UseCutout("__useCutout"Float) = 0

        [Toggle] _AddOutline("__addOutline"Float) = 0
    }

     SubShader
    {
        Pass
        {
            Blend [_SrcBlend] [_DstBlend]
            Cull [_Cull]
            Lighting Off
            ZWrite [_ZWrite]

            Name "Main"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma shader_feature _ _USECUTOUT_ON

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;

            #ifdef _USECUTOUT_ON
                fixed _Cutoff;
            #endif

            struct appdata
            {
                float4 position : POSITION;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 position : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };

            v2f vert(appdata v)
            {
                v2f o;
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                o.position = mul(UNITY_MATRIX_MVP, v.position);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.texcoord) * _Color;

                #ifdef _USECUTOUT_ON
                    clip( col.a - _Cutoff);
                #endif

                return col;
            }
            ENDCG
        }
    }

    CustomEditor "TestShaderGUI"

}


2つ目のシェーダーコードです。

Outlineの部分はStandardAssetsのToonBasicを参考にしてます。

ここでのちょっとしたポイントはUsePassです。

普通に1つ目のシェーダーのコードを書いちゃってもいいですが、

もし1つ目のシェーダーを改造したくなった時にそちらをいじるとこちらにも同じ修正をしないといけません。

今回の場合は2つしかないのでまだいいですが、これが増えてくるとその修正だけで大変です。

なので、UsePassを使います。

これを使うには、参照元のシェーダーのパス内にName "Test"のように書いておいて、

参照先のシェーダーファイルの中でUsePass "参照ファイルのパス + そのパス名" を書きます。

 

例えば、この2つめのシェーダーのアウトラインのパスを別のシェーダーで使いたい場合は

 

UsePass "Hidden/Custom/TestShader_Outline/OUTLINE"

 

と書きます。

注意点はNameではOutlineとつけていても、UsePassで指定するときは全部大文字で書かないといけない点です!

 

Shader "Hidden/Custom/TestShader_Outline"
{
    Properties
    {
        _MainTex("Main Tex"2D) = "white" {}
        _Color("Color"Color) = (1.01.01.01.0)

        _Cutoff("Alpha Cutoff"Range(0.01.0)) = 0.5

        [KeywordEnum(OffFrontBack)] _Cull ("Culling"Float) = 2 

        [HideInInspector] _Mode ("__mode"Float) = 0.0
        [HideInInspector] _SrcBlend ("__src"Float) = 1.0
        [HideInInspector] _DstBlend ("__dst"Float) = 0.0
        [Toggle] _ZWrite ("__zw"Float) = 1.0
        [HideInInspector][Toggle] _UseCutout("__useCutout"Float) = 0

        [Toggle] _AddOutline("__addOutline"Float) = 0
        _OutlineColor ("Outline Color"Color) = (0.50.50.50.5)
        _OutlineWidth ("Outline Width"Range(0.01.0)) = 0.1
    }

    SubShader
    {
        UsePass "Custom/TestShader/MAIN"

        //Outline パス
        Pass
        {
            Name "Outline"
            Cull Front
            Blend [_SrcBlend] [_DstBlend]
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 position : POSITION;
                float3 normal   : NORMAL;
            };

            struct v2f
            {
                float4 position : SV_POSITION;
            };

            fixed4 _OutlineColor;
            fixed _OutlineWidth;

            v2f vert(appdata v)
            {   
                v2f o;
                UNITY_INITIALIZE_OUTPUT(v2f, o);

                o.position = mul(UNITY_MATRIX_MVP, v.position);

                float3 nml = normalize(mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal));
                float2 offset = TransformViewToProjection(nml.xy);

                o.position.xy += offset * o.position.z * _OutlineWidth / 10;

                return o;
            }
            
            fixed4 frag(v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
    CustomEditor "TestShaderGUI"
}


上記2つのシェーダーコードの最後に CutomEditor "TestShaderGUI" と書いてますが、

これを書くことによって、このシェーダーファイルはInspector上の表示に

TestShaderGUIのスクリプトを使用するということになります。

 

ポイント①

StandardShaderGUI.csでもやっているようにFindPropertiesの関数内で

シェーダーで設定しているプロパティの名前を検索する処理を書いています。

ここのFindPropertyの第一引数はシェーダーで設定している変数の方を書きます。

例:_Hoge("Main Tex"2D) なら FindProperty("_Hoge", props)

 

ポイント②

Outlineをつけるかどうかを_AddOutlineというプロパティの0か1で決めていますが、

 1つ目のシェーダーには使わないので_OutlineColorや_OutlineWidthを設定してません。

なので、addOutline.floatValue > 0 のときだけFindPropertyで取得し、

それらのカラープロパティやレンジプロパティを表示するようにしています。

 

ポイント③

1つのシェーダー内でシェーダーバリアントを使って機能のONOFFをできる場合は単純なのですが、

今回のサンプルの様に別パスに分けて描画したいシェーダーと切り替える場合は

m_MaterialEditor.SetShader()

でシェーダー変更しないといけません。

ここは自分が少しハマったところでmaterial.Shader = shader だと変更できませんでした。

 

ポイント④

ビルトインシェーダー内のStandardShaderGUI.csでは定義しているBlendModeのEnumの通りにPopupが表示されるのでOpaqueとかFade、Additiveなど英語でしか見れません。

かといって、Enumの定義に日本語は使えませんので、そこも使ってもらう人に分かりやすくするために

 ChangeDisplayBlendName という関数を作って、そこで表示する用のstring配列に変換しています。

 

using System;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public class TestShaderGUI : ShaderGUI
{
    public enum BlendMode
    {
        Opaque,
        Cutout,
        Fade,       
        Transparent,
        Additive,
        Subtractive,
        Modulate,
        Overlay,
        AdditiveOverlay
    }

    public enum CullMode
    {
        Off,
        Front,
        Back
    }

    private static class Styles
    {      
        public static readonly string[] blendNames = Enum.GetNames (typeof (BlendMode));
        public static readonly string[] cullNames  = Enum.GetNames (typeof (CullMode));
    }

    private static MaterialProperty blendMode    = null;
    private static MaterialProperty cullMode     = null;
    private static MaterialProperty zWriteMode   = null;
    private static MaterialProperty mainTex      = null;
    private static MaterialProperty mainColor    = null;

    private static MaterialProperty useCutout    = null;
    private static MaterialProperty alphaCutout  = null;

    private static MaterialProperty addOutline   = null;
    private static MaterialProperty outlineColor = null;
    private static MaterialProperty outlineWidth = null;


    private static MaterialEditor m_MaterialEditor;
    private Material targetMaterial
    {
        get { return (m_MaterialEditor == null) ? null : m_MaterialEditor.target as Material; } 
    }

    private bool m_FirstTimeApply = true;

    private static Shader testShader         = Shader.Find("Custom/TestShader");
    private static Shader testShader_Outline = Shader.Find("Hidden/Custom/TestShader_Outline");

    public void FindProperties (MaterialProperty[] props)
    {
        blendMode     = FindProperty ("_Mode", props);
        cullMode      = FindProperty ("_Cull", props);
        zWriteMode    = FindProperty ("_ZWrite", props);

        mainTex       = FindProperty ("_MainTex", props);
        mainColor     = FindProperty ("_Color", props);

        alphaCutout   = FindProperty ("_Cutoff", props);
        useCutout     = FindProperty ("_UseCutout", props);

        addOutline    = FindProperty ("_AddOutline", props);

        if(addOutline.floatValue > 0)
        {
            try
            {
                outlineColor = FindProperty ("_OutlineColor", props, false);
                outlineWidth = FindProperty ("_OutlineWidth", props, false);
            }
            catch(Exception e)
            {
                Debug.Log(e);
            }
        }
        else
        {
            outlineColor = null;
            outlineWidth = null;
        }
    }

    public override void OnGUI (MaterialEditor materialEditor, MaterialProperty[] props)
    {
        m_MaterialEditor  = materialEditor;
        Material material = materialEditor.target as Material;

        FindProperties (props);

        if (m_FirstTimeApply)
        {
            MaterialChanged(material);
            m_FirstTimeApply = false;
        }

        ShaderPropertiesGUI(material, props);      
    }

    public void ShaderPropertiesGUI (Material material, MaterialProperty[] props)
    {
        EditorGUIUtility.labelWidth = 0f;
        
        EditorGUI.BeginChangeCheck();
        {
            BlendModePopup();

            if(((BlendMode)material.GetFloat("_Mode") == BlendMode.Cutout))
            {
                m_MaterialEditor.ShaderProperty(
                    alphaCutout,
                    "カットアウトのしきい値",
                    MaterialEditor.kMiniTextureFieldLabelIndentLevel
                );
            }
            else if(((BlendMode)material.GetFloat("_Mode") == BlendMode.Opaque))
            {
                // メニュー追加なし
            }
            else
            {
                m_MaterialEditor.ShaderProperty(zWriteMode, "ZWrite OnOff");
            }

            CullModePopup();

            GUILayout.Box("", new GUILayoutOption[]{GUILayout.ExpandWidth(true), GUILayout.Height(2.0f)});

            m_MaterialEditor.TextureProperty(mainTex, "Texture");
            m_MaterialEditor.ColorProperty(mainColor, "Color");

            GUILayout.Box("", new GUILayoutOption[]{GUILayout.ExpandWidth(true), GUILayout.Height(2.0f)});

            m_MaterialEditor.ShaderProperty(addOutline, "Add Outline");

            if(addOutline.floatValue > 0)
            {
                m_MaterialEditor.SetShader(testShader_Outline);

                FindProperties (props);

                if(outlineColor != null)
                {
                    m_MaterialEditor.ColorProperty(outlineColor, "Outline Color");
                }

                if(outlineWidth != null)
                {
                    m_MaterialEditor.RangeProperty(outlineWidth, "Outline Width");
                }
            }
            else
            {
                m_MaterialEditor.SetShader(testShader);
            }
        }
        if (EditorGUI.EndChangeCheck())
        {
            // CutoutのONOFF ===================================================
            if ((BlendMode)material.GetFloat("_Mode") == BlendMode.Opaque)
            {
                zWriteMode.floatValue = 1.0f;
                useCutout.floatValue = 0;
                material.DisableKeyword("_USECUTOUT_ON");
            }
            else if((BlendMode)material.GetFloat("_Mode") == BlendMode.Cutout)
            {
                zWriteMode.floatValue = 1.0f;
                useCutout.floatValue = 1.0f;
                material.EnableKeyword("_USECUTOUT_ON");
            }
            else
            {
                useCutout.floatValue = 0;
                material.DisableKeyword("_USECUTOUT_ON");
            }

            m_MaterialEditor.PropertiesChanged();

            if(m_MaterialEditor.targets != null && m_MaterialEditor.targets.Length > 0)
            {
                foreach(UnityEngine.Object t in m_MaterialEditor.targets)
                {
                    EditorUtility.SetDirty(t);
                }
            }
            else
            {
                EditorUtility.SetDirty(targetMaterial);
            }
        }
    }

    static void MaterialChanged(Material material)
    {
        SetupMaterialWithBlendMode(material, (BlendMode)material.GetFloat("_Mode"));
    }

    public override void AssignNewShaderToMaterial (Material material, Shader oldShader, Shader newShader)
    {   
        base.AssignNewShaderToMaterial(material, oldShader, newShader);

        if(oldShader != newShader)
        {
            if (oldShader == null )
            {
                SetupMaterialWithBlendMode(material, (BlendMode)material.GetFloat("_Mode"));
                return;
            }
            if( oldShader.name == "Custom/TestShader" || oldShader.name == "Hidden/Custom/testShader_Outline")
            {
                return;
            }
        }
        MaterialChanged(material);
    }

    void BlendModePopup()
    {
        EditorGUI.showMixedValue = blendMode.hasMixedValue;
        var mode = (BlendMode)blendMode.floatValue;

        var dispNames = ChangeDisplayBlendName(Styles.blendNames);

        EditorGUI.BeginChangeCheck();      
        mode = (BlendMode)EditorGUILayout.Popup("Rendering Mode", (int)mode, dispNames);
        if (EditorGUI.EndChangeCheck())
        {
            m_MaterialEditor.RegisterPropertyChangeUndo("Rendering Mode");
            blendMode.floatValue = (float)mode;
        }
        EditorGUI.showMixedValue = false;
    }

    void CullModePopup()
    {
        EditorGUI.showMixedValue = cullMode.hasMixedValue;
        var mode = (CullMode)cullMode.floatValue;

        EditorGUI.BeginChangeCheck();
        mode = (CullMode)EditorGUILayout.Popup("Culling", (int)mode, Styles.cullNames);
        if (EditorGUI.EndChangeCheck())
        {
            m_MaterialEditor.RegisterPropertyChangeUndo("Culling Mode");
            cullMode.floatValue = (float)mode;
        }
        EditorGUI.showMixedValue = false;
    }

    private static void SetupMaterialWithBlendMode(Material material, BlendMode blendMode)
    {
        switch (blendMode)
        {
            case BlendMode.Opaque:
                material.SetOverrideTag("RenderType", "Opaque");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);            
                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.renderQueue = (int)UnityEngine.Rendering.RenderQueue.AlphaTest;         
                break;
            case BlendMode.Fade:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);            
                material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
            case BlendMode.Transparent:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);            
                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.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
            case BlendMode.Subtractive:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcColor);
                material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
            case BlendMode.Modulate:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.DstColor);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
            case BlendMode.Overlay:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
            case BlendMode.AdditiveOverlay:
                material.SetOverrideTag("RenderType", "Transparent");
                material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
                material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
                material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
                break;
        }
    }

    private static string[] ChangeDisplayBlendName(string[] blendNames)
    {
        string[] tempNameArray = new string[blendNames.Length];

        for(var i = 0; i < blendNames.Length; i++)
        {
            if(blendNames[i] == "Opaque")
            {
                tempNameArray[i] = "Opaque (不透明)"; 
            }
            else if(blendNames[i] == "Cutout")
            {
                tempNameArray[i] = "Cutout (2値化抜き)"; 
            }
            else if(blendNames[i] == "Fade")
            {
                tempNameArray[i] = "Fade (透明 通常)"; 
            }
            else if(blendNames[i] == "Transparent")
            {
                tempNameArray[i] = "Transparent (透明 プラスチック・ガラス向き)"; 
            }
            else if(blendNames[i] == "Additive")
            {
                tempNameArray[i] = "Additive (加算)"; 
            }
            else if(blendNames[i] == "Subtractive")
            {
                tempNameArray[i] = "Subtractive (減算)"; 
            }
            else if(blendNames[i] == "Modulate")
            {
                tempNameArray[i] = "Modulate (乗算)"; 
            }
            else if(blendNames[i] == "Overlay")
            {
                tempNameArray[i] = "Overlay (オーバーレイ)"; 
            }
            else if(blendNames[i] == "AdditiveOverlay")
            {
                tempNameArray[i] = "AdditiveOverlay (加算オーバーレイ)"; 
            }
        }
        return tempNameArray;
    }
}

自分的にはある程度シンプルにしたつもりですが、

分かりづらいとこや間違いがあればこちらやTwitterにメッセージください~(・∀・)丿