koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

lilToonのカスタムシェーダーを作るときのTips

基本

基本的なカスタムシェーダーの情報は下記公式ドキュメントを参照すること.

lilToon カスタムシェーダーの作り方

ドキュメント中にある下記のファイルがテンプレートであり,この中の数ファイルを編集する.

目次

custom.hlsl, custom_insert.hlsl について

知っておくべき文法

カスタムシェーダーはプリプロセッサを用いて,本体側の特定位置にカスタムシェーダーで記述した処理を差し込んだシェーダーを作成するようになっている. そのため,プリプロセスについてC/C++の文献等をあたり,基本的な文法は知っておいた方がよい.

プリプロセッサとともによく併用されるのが行末の \ である. これは次の行もその行にあるように扱う「行継続」の意味を持っている. 換言するならば改行の打ち消しである.

#define#if 等のプリプロセッサ指令は1行のみ有効であるため,本来は1行で記述しなければならない. しかし長い1行というのは視認性が悪い. 行継続を用いることで,視認性のために改行を行いつつも,コンパイラには1行として扱うように指示することができる.

カスタムシェーダーの全体図

カスタムシェーダーは下記のような順で各ファイルのインクルードが行われる.

各レンダーパイプライン,パスでどのようなシェーダーとなるかは Assets/lilToon/CustomShaderResources 以下のファイルを参照. 例えば,BRP用のデフォルトのシェーダーは Assets/lilToon/CustomShaderResources/BRP/Default.lilblockを参照するとよい.

また具体的に生成されるシェーダーはシェーダーファイルクリック時のインスペクタに表示される「Export Shader」ボタンから保存することができる.

lilToonカスタムシェーダのExport

custom.hlsl の注意点

基本的には処理置き換えのマクロにとどめておくこと. 関数定義もできるが,インクルード位置がuniform変数の宣言位置(custom.hlsl 内で記述している LIL_CUSTOM_PROPERTIES のマクロの展開箇所も含む)よりも前なので,uniform変数に依存する処理は書けないuniform変数に依存する かどうかに関わらず,関数定義は custom_insert.hlsl で行うように統一すると,問題は起こらないとも言える(好みの問題).

フラグメントシェーダーにおけるデフォルトの各処理

各処理の挿入位置

custom.hlsl, custom_insert.hlsl の変更が反映されない

Reimportを行うこと. 以前にコンパイルエラーがあった場合は,下記のコンパイルエラーがキャッシュされている件の解消方法を試した後にReimportを行うこと.

シェーダーのコンパイルエラー内容が古い内容のまま発生する

カスタムシェーダーは変にキャッシュが残ることがあり,custom.hlsl, custom_insert.hlsl を正しく修正してもコンパイルエラーが取れないことがある. この現象が発生すると,インスペクタの値の変更がプレビューに反映されない,インスペクタでエラーとなっているバリエーションのシェーダーが表示されず選択できない,等の現象が発生する.

この現象を解決するためには Library/ShaderCache.db を適当な sqlite3 クライアントで開き,下記のSQLを実行する.

DELETE FROM shadererrors;

sqlite3のコマンドラインツールなら下記のコマンドの実行でよい. (echoで '.exit' を出力するのはWindowsのため.空文字列を出力する方法がないため,受理されるコマンドを出力している.Linuxであれば空文字列でよい)

$ echo .exit | sqlite3 --cmd "DELETE FROM shadererrors;" ShaderCache.db

面倒であれば, Library/ShaderCache.db のファイル削除でもよいのだが,Unity起動中は削除することができない.

winsqlite3.dllが含まれる最近のWindows10や sqlite3 の動的ライブラリにパスが通っているMac/Linux限定にはなるが,下記の処理を設けることで,SQLite3のライブラリの同梱なしに右クリックメニューから DELETE FROM shadererrors を実行できるようにすることもできる.

/// <summary>
/// Callback method for menu item which refreshes shader cache and reimport.
/// </summary>
[MenuItem("Assets/TemplateFull/Refresh shader cache", false, 2000)]
private static void RefreshShaderCacheMenu()
{
    var result = NativeMethods.Open("Library/ShaderCache.db", out var dbHandle);
    if (result != 0)
    {
        Debug.LogError($"Failed to open Library/ShaderCache.db [{result}]");
        return;
    }

    try
    {
        result = NativeMethods.Execute(dbHandle, "DELETE FROM shadererrors", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
        if (result != 0)
        {
            Debug.LogError($"SQL failed [{result}]");
            return;
        }
    }
    finally
    {
        result = NativeMethods.Close(dbHandle);
        if (result != 0)
        {
            Debug.LogError($"Failed to close database [{result}]");
        }
    }

    AssetDatabase.ImportAsset("Assets/TemplateFull/Shaders", ImportAssetOptions.ImportRecursive);
}


/// <summary>
/// Provides some native methods of SQLite3.
/// </summary>
internal static class NativeMethods
{
#if UNITY_EDITOR && !UNITY_EDITOR_WIN
    /// <summary>
    /// Native library name of SQLite3.
    /// </summary>
    private const string LibraryName = "sqlite3";
    /// <summary>
    /// Calling convention of library functions.
    /// </summary>
    private const CallingConvention CallConv = CallingConvention.Cdecl;
#else
    /// <summary>
    /// Native library name of SQLite3.
    /// </summary>
    private const string LibraryName = "winsqlite3";
    /// <summary>
    /// Calling convention of library functions.
    /// </summary>
    private const CallingConvention CallConv = CallingConvention.StdCall;
#endif
    /// <summary>
    /// Open database.
    /// </summary>
    /// <param name="filePath">SQLite3 database file path.</param>
    /// <param name="db">SQLite db handle.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_open", CallingConvention = CallConv)]
    public static extern int Open(string filename, out IntPtr dbHandle);

    /// <summary>
    /// Close database.
    /// </summary>
    /// <param name="filePath">Database filename.</param>
    /// <param name="db">SQLite db handle.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/close.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_close", CallingConvention = CallConv)]
    public static extern int Close(IntPtr db);

    /// <summary>
    /// Execute specified SQL.
    /// </summary>
    /// <param name="db">An open database.</param>
    /// <param name="sql">SQL to be evaluated.</param>
    /// <param name="callback">Callback function.</param>
    /// <param name="callbackArg">1st argument to callback.</param>
    /// <param name="pErrMsg">Error msg written here.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/exec.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_exec", CallingConvention = CallConv)]
    public static extern int Execute(IntPtr dbHandle, string sql, IntPtr callback, IntPtr callbackArg, IntPtr pErrMsg);
}

ただ単にDELETEを実行するだけなので最小限の処理にしている. 真面目にやるのであれば下記のような点も考慮するといい感じになる.

  • sqlite3_exec() の第5引数でエラーメッセージを受け取る
    • sqlite3_free() での解放処理が必要
  • SQLite3のハンドルは SafeHandle の継承クラスで扱う
  • 戻り値の enum を定義する
  • まず sqlite3.dll のロードを試みて,見つからなければ winsqlite3.dll のロードを試みる
    • ユーザにdllを用意してもらうことで使用できるようにする想定
    • x86を考慮して呼び出し規約の切り分け
      • winsqlite3.dll なら stdcall
      • sqlite3.dll なら cdecl
  • SQLite3 のライブラリが見つからない場合はメニューに追加しない
  • Reimport対象のアセットパス直書きではなくGUID経由で取得する
    • unitypackage/VPMの両対応のため

頂点シェーダー→フラグメントシェーダーの受け渡しメンバを定義する

メンバの追加は LIL_CUSTOM_V2F_MEMBER を,値の設定処理は LIL_CUSTOM_VERT_COPY を利用する.

  • custom.hlsl
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0;

// Add vertex copy
#define LIL_CUSTOM_VERT_COPY \
    LIL_V2F_OUT.emissionWavePos = pickupPosition(getEmissionPos(input.positionOS)) \
        + (2.0 * rand(float2((float)input.vertexID, LIL_TIME)) - 1.0) * _EmissionWaveNoiseAmp;

#define BEFORE_BLEND_EMISSION \
    const float uDiff = frac(LIL_TIME * _EmissionWaveTimeScale + _EmissionWaveTimePhase) - remap01(_EmissionPosMin, _EmissionPosMax, input.emissionWavePos); \
    const float sDiff = 2.0 * uDiff - 1.0; \
    const float eFact = pow(0.5 * cos(clamp(sDiff * _EmissionWaveParam.x, -1.0, 1.0) * UNITY_PI) + 0.5, _EmissionWaveParam.y); \
    fd.emissionColor += _EmissionWaveColor * eFact;

LIL_CUSTOM_V2F_MEMBER の引数はTEXCOORDのIDとなるため,## を用いて字句結合を行う. 頂点シェーダー内での出力構造体変数は LIL_V2F_OUT を指定,フラグメントシェーダー内での入力構造体変数は input を指定する.

LIL_CUSTOM_V2F_MEMBER の展開箇所は例えば Assets/lilToon/Shader/Includes/lil_pass_forward_normal.hlsl を参照するとよい. 頂点シェーダーは例えば Assets/lilToon/Shader/Includes/lil_common_vert.hlsl 等を, フラグメントシェーダーは Assets/lilToon/Shader/Includes/lil_pass_forward_normal.hlsl 等を参照するとよい.

lilToon 1.4.0 でのバグとワークアラウンド

本体側で TEXCOORD のIDと重複するIDが LIL_CUSTOM_V2F_MEMBERid0 として渡されているため,コンパイルエラーとなるパスが存在する. ShadowCasterパスでは id0 の使用を避けるようにする. 面倒であればすべてのパスで id0 の使用を避けても良いと思う.

  • NG
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float customMember01 : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float3 customMember03 : TEXCOORD ## id2;
  • OK
#ifdef UNITY_PASS_SHADOWCASTER
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id1; \
    float4 customMember02 : TEXCOORD ## id2; \
    float customMember03 : TEXCOORD ## id3;
#else
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float customMember03 : TEXCOORD ## id2;
#endif

Ver.1.4.1で直った

本体側のバージョンに応じてカスタムシェーダーの条件コンパイルを行う

前述の特定のバージョン向けのワークアラウンド等で本体のバージョンに応じてカスタムシェーダーの処理を変更したいことがある. 本体のシェーダーのマクロ定義にはバージョンに関するものがないが,C#側には存在する. これを利用して,下記のようにしてバージョン値定義マクロを定義したファイルを生成する.

GuidShaderDir はカスタムシェーダーの Shaders ディレクトリのGUIDにすること.

using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEditor;
using UnityEngine;
using lilToon;


namespace lilToon
{
    /// <summary>
    /// Startup method provider.
    /// </summary>
    internal static class Startup
    {
        /// <summary>
        /// Buffer size of streams.
        /// </summary>
        private const int DefaultBufferSize = 1024;
        /// <summary>
        /// GUID of shader directory.
        /// </summary>
        // TODO: Replace to GUID of your "Shader" directory.
        private const string GuidShaderDir = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";  // TODO

        /// <summary>
        /// A method called at Unity startup.
        /// </summary>
        [InitializeOnLoadMethod]
        private static void OnStartup()
        {
            AssetDatabase.importPackageCompleted += Startup_ImportPackageCompleted;
            UpdateVersionDefFile();
        }

        /// <summary>
        /// Update definition file of version value of lilToon, lil_current_version_value.hlsl.
        /// </summary>
        private static void UpdateVersionDefFile()
        {
            var dstDirPath = AssetDatabase.GUIDToAssetPath(GuidShaderDir);
            if (dstDirPath == "")
            {
                Debug.LogWarning("Cannot find file or directory corresponding to GUID: " + GuidShaderDir);
                return;
            }
            if (!Directory.Exists(dstDirPath))
            {
                Debug.LogWarningFormat("Directory not found: {0} ({1})", dstDirPath, GuidShaderDir);
                return;
            }
            UpdateVersionDefFile(Path.Combine(dstDirPath, "lil_current_version.hlsl"));
        }

        /// <summary>
        /// Update definition file of version value of lilToon, lil_current_version_value.hlsl.
        /// </summary>
        /// <param name="filePath">Destination file path.</param>
        /// <param name="bufferSize">Buffer size for temporary buffer and <see cref="FileStream"/>,
        /// and initial capacity of <see cref="MemoryStream"/>.</param>
        public static void UpdateVersionDefFile(string filePath, int bufferSize = DefaultBufferSize)
        {
            using (var ms = new MemoryStream(bufferSize))
            {
                WriteVersionFileBytes(ms);
                var buffer = ms.GetBuffer();
                var length = (int)ms.Length;

                if (CompareFileBytes(filePath, buffer, 0, length, bufferSize))
                {
                    return;
                }

                using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize))
                {
                    fs.Write(buffer, 0, length);
                }

                Debug.LogFormat("Update {0}", filePath);
            }
        }

        /// <summary>
        /// Write version file content to <see cref="s"/>.
        /// </summary>
        /// <param name="s">Destination stream.</param>
        /// <param name="bufferSize">Buffer size for <see cref="StreamWriter"/>.</param>
        private static void WriteVersionFileBytes(Stream s, int bufferSize = DefaultBufferSize)
        {
            using (var writer = new StreamWriter(s, Encoding.ASCII, bufferSize, true))
            {
                writer.Write("#ifndef LIL_CURRENT_VERSION_INCLUDED\n");
                writer.Write("#define LIL_CURRENT_VERSION_INCLUDED\n");
                writer.Write('\n');
                writer.Write("#define LIL_CURRENT_VERSION_VALUE {0}\n", lilConstants.currentVersionValue);

                var verTokens = lilConstants.currentVersionName.Split('.');
                int verNum;
                if (int.TryParse(verTokens[0], out verNum))
                {
                    writer.Write("#define LIL_CURRENT_VERSION_MAJOR {0}\n", verNum);
                }
                if (verTokens.Length > 0 && int.TryParse(verTokens[1], out verNum))
                {
                    writer.Write("#define LIL_CURRENT_VERSION_MINOR {0}\n", verNum);
                }
                if (verTokens.Length > 1 && int.TryParse(verTokens[2], out verNum))
                {
                    writer.Write("#define LIL_CURRENT_VERSION_PATCH {0}\n", verNum);
                }

                writer.Write('\n');
                writer.Write("#endif  // LIL_CURRENT_VERSION_INCLUDED\n");
            }
        }

        /// <summary>
        /// Compare file content with specified byte sequence.
        /// </summary>
        /// <param name="filePath">Target file path.</param>
        /// <param name="contentData">File content data to compare.</param>
        /// <param name="offset">Offset of <paramref name="contentData"/>,</param>
        /// <param name="count">Length of <paramref name="contentData"/>.</param>
        /// <param name="bufferSize">Buffer size for temporary buffer and <see cref="FileStream"/>.</param>
        /// <returns>True if file content is same to <see cref="contentData"/>, otherwise false.</returns>
        private static bool CompareFileBytes(string filePath, byte[] contentData, int offset, int count, int bufferSize = DefaultBufferSize)
        {
            if (!File.Exists(filePath))
            {
                return false;
            }
            if (new FileInfo(filePath).Length != count)
            {
                return false;
            }

            var minBufferSize = Math.Min(count, bufferSize);
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, minBufferSize))
            {
                var buffer = new byte[minBufferSize];
                int nRead;
                while ((nRead = fs.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (!CompareMemory(buffer, 0, contentData, offset, nRead))
                    {
                        return false;
                    }
                    offset += nRead;
                }
            }

            return true;
        }

        /// <summary>
        /// Compare two byte data.
        /// </summary>
        /// <param name="data1">First byte data array.</param>
        /// <param name="offset1">Offset of first byte data array.</param>
        /// <param name="data2">Second byte data array.</param>
        /// <param name="offset2">Offset of second byte data array.</param>
        /// <param name="count">Number of bytes comparing <paramref name="data1"/> and <paramref name="data2"/>.</param>
        /// <returns>True if two byte data is same, otherwise false.</returns>
        private static bool CompareMemory(byte[] data1, int offset1, byte[] data2, int offset2, int count)
        {
            for (int i = 0; i < count; i++)
            {
                if (data1[offset1 + i] != data2[offset2 + i])
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// A callback method for <see cref="AssetDatabase.importPackageCompleted"/>.
        /// </summary>
        /// <param name="packageName">Imported package name.</param>
        private static void Startup_ImportPackageCompleted(string packageName)
        {
            if (!packageName.StartsWith("lilToon"))
            {
                return;
            }
            UpdateVersionDefFile();
        }
    }
}

上記のコードで生成されるのは lil_current_version.hlsl というファイルで,下記のようなたった1行のファイルである.

#ifndef LIL_CURRENT_VERSION_INCLUDED
#define LIL_CURRENT_VERSION_INCLUDED

#define LIL_CURRENT_VERSION_VALUE 42
#define LIL_CURRENT_VERSION_MAJOR 1
#define LIL_CURRENT_VERSION_MINOR 7
#define LIL_CURRENT_VERSION_PATCH 2

#endif  // LIL_CURRENT_VERSION_INCLUDED

lil_current_version.hlslcustom.hlsl の先頭でインクルードする.

#include "lil_current_version.hlsl"

これを用いると前章の1.4.0向けのワークアラウンドは下記のように書き直せる.

#if LIL_CURRENT_VERSION_VALUE == 34 && defined(UNITY_PASS_SHADOWCASTER)
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id1; \
    float4 customMember02 : TEXCOORD ## id2; \
    float customMember03 : TEXCOORD ## id3;
#else
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float customMember03 : TEXCOORD ## id2;
#endif

バージョンとバージョン値(上記の例だと34)の対応関係は以下の表の通り. これは Assets/lilToon/Editor/lilConstants.cs の変更履歴を見ればわかる.

lilToonのバージョン(currentVersionName バージョン値(currentVersionValue
1.3.0 26
1.3.1 27
1.3.2 28
1.3.3 29
1.3.4 30
1.3.5 31
1.3.6 32
1.3.7 33
1.4.0 34
1.4.1 35
1.5.0 36
1.5.1 37
1.6.0 38
1.6.1 39
1.7.0 40
1.7.1 41
1.7.2 42

シェーダーキーワードを使用する

下記5ファイルに #pragma multi_compile#pragma shader_feature_local を記述する. multi版以外ではキーワードがインスペクタの処理で削除されるため,記述しても意味がない.

  • ltsmulti.lilcontainer
  • ltsmulti_fur.lilcontainer
  • ltsmulti_gem.lilcontainer
  • ltsmulti_o.lilcontainer
  • ltsmulti_ref.lilcontainer
    HLSLINCLUDE
        #pragma shader_feature_local _ _TOGGLEPROP_ON
        #pragma shader_feature_local _ENUMKEYWORD_FOO _ENUMKEYWORD_BAR _ENUMKEYWORD_BAZ
        #include "custom.hlsl"
    ENDHLSL

multi版以外でもどうしてもシェーダーキーワードを使用する

lilToonの設計思想に真っ向から対立していると思うが....

まず,前述の5ファイルの代わりに下記ファイルにキーワードのpragmaを記述する.

  • lilCustomShaderInsert.lilblock
#pragma shader_feature_local _ _TOGGLEPROP_ON
#pragma shader_feature_local _ENUMKEYWORD_FOO _ENUMKEYWORD_BAR _ENUMKEYWORD_BAZ
#include "custom_insert.hlsl"

次にインスペクタのコードにて,OnGUI() をオーバーライドし,親クラスの OnGUI() を呼び出し後に,対象のマテリアルにキーワードを設定する処理を追加する. キーワードは DrawCustomProperties() で保存しておく.

/// <summary>
/// Keywords to preserve.
/// </summary>
private List<string> _shaderKeywords = new List<string>();

/// <summary>
/// Draw property items.
/// </summary>
/// <param name="me">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
/// <param name="mps">Material properties of the current selected shader.</param>
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
    base.OnGUI(materialEditor, props);

    foreach (var shaderKeyword in shaderKeywords)
    {
        material.EnableKeyword(keyword);
    }
    _shaderKeywords.Clear();
}

/// <summary>
/// Draw custom properties.
/// </summary>
/// <param name="material">Target material.</param>
protected override void DrawCustomProperties(Material material)
{
    // ...

    _shaderKeywords.Add($"_TOGGLEPROP_{(prop.floatValue >= 0.5f ? "ON" : "OFF")}");

    // ...
}

末検証であるが,lilToonMultiで使用されるキーワードを参考に,雑にやるなら下記のようにしてもよいと思う.

/// <summary>
/// Keywords to preserve.
/// </summary>
private List<string> _shaderKeywords = new List<string>();

/// <summary>
/// Draw property items.
/// </summary>
/// <param name="me">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
/// <param name="mps">Material properties of the current selected shader.</param>
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
    base.OnGUI(materialEditor, props);

    foreach (var shaderKeyword in shaderKeywords)
    {
        material.EnableKeyword(keyword);
    }
    _shaderKeywords.Clear();
}

/// <summary>
/// Draw custom properties.
/// </summary>
/// <param name="material">Target material.</param>
protected override void DrawCustomProperties(Material material)
{
    // ...

    // MUSE BE write the end of this method.
    foreach (var keyword in material.shaderKeywords)
    {
        if (!IsMultiKeyword(keyword))
        {
            _shaderKeywords.Add(keyword);
        }
    }
}

/// <summary>
/// Check keyword is used in lilToonMulti or not.
/// </summary>
/// <param name="keyword">Shader keyword.</param>
/// <returns>True if the keyword is used in lilToonMulti, otherwise false.</returns>
private static bool IsMultiKeyword(string keyword)
{
    switch (keyword)
    {
        case "ANTI_FLICKER":
        case "EFFECT_BUMP":
        case "EFFECT_HUE_VARIATION":
        case "ETC1_EXTERNAL_ALPHA":
        case "GEOM_TYPE_BRANCH":
        case "GEOM_TYPE_BRANCH_DETAIL":
        case "GEOM_TYPE_FROND":
        case "GEOM_TYPE_LEAF":
        case "GEOM_TYPE_MESH":
        case "PIXELSNAP_ON":
        case "UNITY_UI_ALPHACLIP":
        case "UNITY_UI_CLIP_RECT":
        case "_COLORADDSUBDIFF_ON":
        case "_COLORCOLOR_ON":
        case "_COLOROVERLAY_ON":
        case "_DETAIL_MULX2":
        case "_EMISSION":
        case "_FADING_ON":
        case "_GLOSSYREFLECTIONS_OFF":
        case "_MAPPING_6_FRAMES_LAYOUT":
        case "_METALLICGLOSSMAP":
        case "_NORMALMAP":
        case "_PARALLAXMAP":
        case "_REQUIRE_UV2":
        case "_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A":
        case "_SPECGLOSSMAP":
        case "_SPECULARHIGHLIGHTS_OFF":
        case "_SUNDISK_HIGH_QUALITY":
        case "_SUNDISK_NONE":
        case "_SUNDISK_SIMPLE":
            return true;
        default:
            return false;
    }
}

AudioLinkの処理を書きたい

lilToonの本体側での _AudioTexture の宣言の有無はAudioLink機能が有効か無効であるかに依存する.

本体側のAudioLink機能が有効か無効であるかに左右されないようにするためには, custom_insert.hlsl 内で下記のように宣言すること.

  • custom_insert.hlsl
// _AudioTexture is declared in lil_common_input.hlsl.
#ifndef LIL_FEATURE_AUDIOLINK
TEXTURE2D_FLOAT(_AudioTexture);
float4 _AudioTexture_TexelSize;
#endif  // LIL_FEATURE_AUDIOLINK

uniform変数の宣言は custom.hlsl 内の LIL_CUSTOM_PROPERTIESLIL_CUSTOM_TEXTURES マクロで行うべきと思うかもしれないが, custom.hlsl の段階では LIL_FEATURE_AUDIOLINK マクロが定義されていないため不可能である.

VRChatのカメラ判定,ミラー判定のuniform変数

下記3つの変数はVRChat側から提供されているuniform変数であり,どのワールドでも使用することができる.

  • _VRChatCameraMode
  • _VRChatMirrorMode
  • _VRChatMirrorCameraPos

lilToon 1.7.2現在では本体側シェーダーで宣言されていないため,custom.hlslLIL_CUSTOM_PROPERTIES を用いて宣言して問題ない.

#define LIL_CUSTOM_PROPERTIES \
    float _VRChatCameraMode; \
    float _VRChatMirrorMode; \
    float3 _VRChatMirrorCameraPos;

もし本体側で宣言された場合はAudioLinkと同じ対応をとること. 本体のissueにミラー表示制御を実装してほしいという要望があるため,将来的に追加される可能性がある.

シェーダー側でmultiシェーダーかどうかを判定する

マクロ LIL_MULTI が定義されているかどうかを調べる. ただし,このマクロは custom.hlsl の段階では定義されておらず, custom_insert.hlsl の段階でないと使用できないことに注意.

#ifdef LIL_MULTI
// マルチシェーダー用の処理
#endif

非multiとmulti版である程度コードを共有する

非multi版でif文を用い,multi版で条件コンパイルを用いると,同じコードを2度書くことになる.

  • lilCustomShaderProperties.lilblock
        //----------------------------------------------------------------------------------------------------------------------
        // Custom Properties
        [Toggle] _ToggleProp ("Toggle Property", Int) = 0
        [KeywordEnum(Foo, Bar, Baz)] _KeywordEnumProp ("Keyword enum property", Int) = 0
  • custom.hlsl
#define LIL_CUSTOM_PROPERTIES \
    bool _ToggleProp; \
    int _KeywordEnumProp;
  • custom_insert.hlsl
float4 getColor()
{
#if !defined(LIL_MULTI)
    if (_ToggleProp) {
        return float4(1.0, 0.0, 0.0, 1.0);
    } else {
        return float4(0.0, 1.0, 0.0, 1.0);
    }
#elif defined(_TOGGLEPROP_ON)
    return float4(1.0, 0.0, 0.0, 1.0);
#else
    return float4(0.0, 1.0, 0.0, 1.0);
#endif
}

float selectElement(float3 v)
{
#if !defined(LIL_MULTI)
    if (_KeywordEnumProp == 0) {
        return v.x;
    } else if (_KeywordEnumProp == 1) {
        return v.y;
    } else {
        return v.z;
    }
#elif defined(_KEYWORDENUMPROP_FOO)
    return v.x;
#elif defined(_KEYWORDENUMPROP_BAR)
    return v.y;
#elif defined(_KEYWORDENUMPROP_BAZ)
    return v.z;
#endif
}

[Toggle][KeywordEnum] に対するuniform変数を用意し,if文で条件分岐を記述する. マルチシェーダー,すなわち LIL_MULTI が定義されている場合のみ,uniform変数をマクロによって定数に置換し, if文の条件分岐がコンパイル時に確定するようにし,プリプロセス段階ではなくコンパイル段階での不要な処理の除去をコンパイラに任せる.

なお, custom.hlsl の段階では LIL_MULTI が定義されていないので,マルチシェーダーのときはuniform変数を定義しない,ということは諦める.

  • custom_insert.hlsl
#ifdef LIL_MULTI
#    ifdef _TOGGLEPROP_ON
#        define _ToggleProp true
#    else
#        define _ToggleProp false
#    endif  // _TOGGLEPROP_ON
#    if defined(_KEYWORDENUMPROP_FOO)
#        define _KeywordEnumProp 0
#    elif defined(_KEYWORDENUMPROP_BAR)
#        define _KeywordEnumProp 1
#    elif defined(_KEYWORDENUMPROP_BAZ)
#        define _KeywordEnumProp 2
#    endif
#endif  // LIL_MULTI

float4 getColor()
{
    if (_ToggleProp) {
        return float4(1.0, 0.0, 0.0, 1.0);
    } else {
        return float4(0.0, 1.0, 0.0, 1.0);
    }
}

float selectElement(float3 v)
{
    if (_KeywordEnumProp == 0) {
        return v.x;
    } else if (_KeywordEnumProp == 1) {
        return v.y;
    } else {
        return v.z;
    }
}

SV_POSITION に NaN

頂点シェーダーの出力構造体で SV_POSITION に相当するメンバに NaN を代入することで,頂点に関連するポリゴンを消去するテクニック. フラグメントシェーダーに渡ってから discard するよりおそらくGPUにやさしい.

custom.hlsl で下記のようにする(VRChatのカメラに写らなくするシェーダー例).

#define LIL_CUSTOM_VERT_COPY
    if (_VRChatCameraMode != 0.0) { \
        LIL_INITIALIZE_STRUCT(v2f, LIL_V2F_OUT_BASE); \
        LIL_V2F_OUT_BASE.positionCS = 0.0 / 0.0; \
        return LIL_V2F_OUT; \
    }

LIL_INITIALIZE_STRUCT を入れておくことで,コンパイラの最適化処理により,頂点シェーダーの先頭あたりに上記のコードを記述したのと同一のコードが生成される. すなわち,より早い段階でearly returnを行うことができる. LIL_V2F_OUT_BASE.positionCSfloat4 であるが, 0.0 / 0.0 は全要素 0.0 / 0.0float4 に暗黙的に変換されるのを利用している. 後続の処理は不要なので,returnしておく.

NaNの警告回避

本体側で4008番の警告を無効化するpragma定義がされているので不要.

0.0 / 0.0 と書きたくないなら asfloat(0x7fc00000) と書いてもよい.

マテリアルエディタ

キーワードについて

マルチシェーダーでない限りキーワードは削除されるので注意.

マルチシェーダーかどうかの判定

lilToon.lilInspector に定義されている静的メンバ isMulti を参照する.

if (isMulti)
{
    material.EnableKeyword("_TOGGLEPROP_ON");
}

シェーダー名に Multi が含まれるかどうかで判定する手もある. 自前で定義したDrawer内では isMulti は参照できないため,シェーダー名で判断するしかない?

protected readonly string _keyword;

public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor)
{
    var isOn = prop.floatValue >= 0.5f;
    var kw = string.IsNullOrEmpty(_keyword) ? prop.name.ToUpperInvariant() + "_ON" : _keyword;

    foreach (Material material in prop.targets.Where(material => material.shader.name.IndexOf("Multi", material.shader.name.LastIndexOf('/')) != -1))
    {
        if (isOn)
        {
            material.EnableKeyword(kw);
        }
        else
        {
            material.DisableKeyword(kw);
        }
    }
}

多言語対応

公式の作例の lilToonGeometryFX を参照.

多言語ファイルは下記のような1行目がヘッダ行(言語名),2行目以降がデータ行のTSVファイル. データ行の1列名はキーで,2列目以降が各言語に応じた文言である.

ファイル名は何でもよい(GUIDで参照するため). 作例に習うなら lang_custom.txt . GUIDは lang_custom.txt.meta を参照すること.

Language  English Japanese    Korean  Chinese Simplified  Chinese Traditional
sCustomGeometryAnimation    Geometry Animation  ジオメトリアニメーション    지오메트리 애니메이션 Geometry Animation  Geometry Animation
sCustomBase Base Setting    基本設定    기본 설정   基本设置    基本設置
sCustomVector   Vector  向き  방향  向量  向量
sCustomDelay    Delay   ディレイ    딜레이   延迟  延遲
sCustomSpeed    Speed   速度  속도  速度  速度
sCustomRandomize    Randomize   ランダム化 임의화   随机化   隨機化
sCustomNormal   Normal  法線  노멀  法线  法線
sCustomOffset   Offset  オフセット Offset  Offset  Offset
sCustomNormalMap    Normal Map  ノーマルマップ   노멀 맵  法线贴图    法線貼圖
sCustomStrength Strength    強度  강도  强度  強度
sCustomShrink   Shrink  縮小  축소  缩减  縮減
sCustomMotionNormal Motion Normal   モーション法線   모션 법선   运动法线    運動法線
sCustomShadingNormal    Shading Normal  シェーディング法線 셰이딩 법선    着色法线    著色法線
sCustomGenerateSide Generate Side   側面を生成 측면 생성   生成侧面    生成側面

C# 側では LoadCustomLanguage() メソッドでファイルを読み込み, GetLoc() メソッドでキーを指定してローカライズされた文言を取得する. もし,定義されていないキーであった場合.GetLoc()キー名をそのまま返す

protected override void LoadCustomProperties(MaterialProperty[] props, Material material)
{
    // ...

    LoadCustomLanguage("a5875813c34e16a49ae1c8e1a846ea75");

    // ...
}

protected override void DrawCustomProperties(Material material)
{
    // ...

    var label = GetLoc("sCustomGeometryAnimation");

    // ...
}

ToggleLeftによる折り畳みとキーワード

シェーダー側で [Toggle] を指定しているプロパティ(MaterialToggleDrawer)について,lilToon本体の折り畳みに合わせ,なおかつキーワードを定義したい場合の解決法. (当たり前のことではあるが,Drawerを定義して,そのDrawerを指定する方が良いとは思う.)

下記のように記載した場合, EditorGUI.ToggleLeft が使用されないため不恰好になる.

m_MaterialEditor.ShaderProperty(_toggleProp, "Label for toggle property");
  • UnityEditor.MaterialEditor.ShaderProperty
    • UnityEditor.MaterialEditor.ShaderPropertyInternal

しかし,UnityEditor.MaterialEditor.ShaderProperty() で行われている処理である MaterialProperty に設定されている Drawer を取得し,その DrawerOnGUI() を呼び出すのは, 使用されているクラス・メソッド類が外部からは private となっているため,リフレクションを活用する必要がある.

// 下記のusing必要
using System.Reflection;

// ...

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop)
{
    SetToggleKeyword(shader, prop, prop.floatValue >= 0.5f);
}

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop, bool isOn)
{
    // Get assembly from public class.
    var asm = Assembly.GetAssembly(typeof(UnityEditor.MaterialPropertyDrawer));

    // Get type of UnityEditor.MaterialPropertyHandler which is the internal class.
    var typeMph = asm.GetType("UnityEditor.MaterialPropertyHandler")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialPropertyHandler");
    var miGetHandler = typeMph.GetMethod(
        "GetHandler",
        BindingFlags.NonPublic
            | BindingFlags.Static)
        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialPropertyHandler.GetHandler");

    // Instance of UnityEditor.MaterialPropertyHandler.
    var handler = miGetHandler.Invoke(null, new object[]
    {
        shader,
        prop.name
    });

    var pi = typeMph.GetProperty(
        "propertyDrawer",
        BindingFlags.GetProperty
            | BindingFlags.Public
            | BindingFlags.Instance)
        ?? throw new InvalidOperationException("PropertyInfo not found: UnityEditor.MaterialPropertyHandler.propertyDrawer");
    var drawer = pi.GetValue(handler)
        ?? throw new InvalidOperationException("Field not found: UnityEditor.MaterialPropertyHandler.propertyDrawer");


    // Check if drawer is instance of UnityEditor.MaterialToggleUIDrawer or not.
    var typeMtd = asm.GetType("UnityEditor.MaterialToggleUIDrawer")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialToggleUIDrawer");
    if (!drawer.IsSubClassOf(typeMtd))
    {
        throw new ArgumentException($"{nameof(prop)} is not instance of UnityEditor.MaterialToggleUIDrawer.");
    }

    var miSetKeyword = typeMtd.GetMethod(
        "SetKeyword",
        BindingFlags.NonPublic
            | BindingFlags.Instance)
        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialToggleUIDrawer.SetKeyword");
    miSetKeyword.Invoke(drawer, new object[]
    {
        prop,
        isOn
    });
}

リフレクション結果のキャッシュを作るのであれば下記のようにするとよい(クリックで開く閉じる).

// 下記のusing必要
using System.Linq.Expression;
using System.Reflection;

// ...

/// <summary>
/// Cache of reflection result of following lambda.
/// </summary>
/// <remarks><seealso cref="CreateToggleKeywordDelegate"/></remarks>
private static Action<Shader, MaterialProperty, bool> _toggleKeyword;

// ...

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop)
{
    SetToggleKeyword(shader, prop, prop.floatValue >= 0.5f);
}

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
/// <param name="isOn">True to enable (define) keyword, false to disable (undefine) keyword.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop, bool isOn)
{
    try
    {
        (_toggleKeyword ?? (_toggleKeyword = CreateSetKeywordDelegate()))(shader, prop, isOn);
    }
    catch (Exception ex)
    {
        Debug.LogError(ex.ToString());
    }
}

/// <summary>
/// <para>Create delegate of reflection results about UnityEditor.MaterialToggleUIDrawer.</para>
/// <code>
/// (Shader shader, MaterialProperty prop, bool isOn) =>
/// {
///     MaterialPropertyHandler mph = UnityEditor.MaterialPropertyHandler.GetHandler(shader, prop.name);
///     if (mph is null)
///     {
///         throw new ArgumentException("Specified MaterialProperty does not have UnityEditor.MaterialPropertyHandler");
///     }
///     MaterialToggleUIDrawer mpud = mph.propertyDrawer as MaterialToggleUIDrawer;
///     if (mpud is null)
///     {
///         throw new ArgumentException("Specified MaterialProperty does not have UnityEditor.MaterialToggleUIDrawer");
///     }
///     mpud.SetKeyword(prop, isOn);
/// }
/// </code>
/// </summary>
private static Action<Shader, MaterialProperty, bool> CreateSetKeywordDelegate()
{
    // Get assembly from public class.
    var asm = Assembly.GetAssembly(typeof(UnityEditor.MaterialPropertyDrawer));

    // Get type of UnityEditor.MaterialPropertyHandler which is the internal class.
    var typeMph = asm.GetType("UnityEditor.MaterialPropertyHandler")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialPropertyHandler");
    var typeMtud = asm.GetType("UnityEditor.MaterialToggleUIDrawer")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialToggleUIDrawer");

    var ciArgumentException = typeof(ArgumentException).GetConstructor(new[] {typeof(string)});

    var pShader = Expression.Parameter(typeof(Shader), "shader");
    var pMaterialPropertyHandler = Expression.Parameter(typeMph, "mph");
    var pMaterialToggleUIDrawer = Expression.Parameter(typeMtud, "mtud");
    var pMaterialProperty = Expression.Parameter(typeof(MaterialProperty), "mp");
    var pBool = Expression.Parameter(typeof(bool), "isOn");

    var cNull = Expression.Constant(null);

    return Expression.Lambda<Action<Shader, MaterialProperty, bool>>(
        Expression.Block(
            new[]
            {
                pMaterialPropertyHandler,
                pMaterialToggleUIDrawer
            },
            Expression.Assign(
                pMaterialPropertyHandler,
                Expression.Call(
                    typeMph.GetMethod(
                        "GetHandler",
                        BindingFlags.NonPublic
                            | BindingFlags.Static)
                        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialPropertyHandler.GetHandler"),
                    pShader,
                    Expression.Property(
                        pMaterialProperty,
                        typeof(MaterialProperty).GetProperty(
                            "name",
                            BindingFlags.GetProperty
                                | BindingFlags.Public
                                | BindingFlags.Instance)))),
            Expression.IfThen(
                Expression.Equal(
                    pMaterialPropertyHandler,
                    cNull),
                Expression.Throw(
                    Expression.New(
                        ciArgumentException,
                        Expression.Constant("Specified MaterialProperty does not have UnityEditor.MaterialPropertyHandler")))),
            Expression.Assign(
                pMaterialToggleUIDrawer,
                Expression.TypeAs(
                    Expression.Property(
                        pMaterialPropertyHandler,
                        typeMph.GetProperty(
                            "propertyDrawer",
                            BindingFlags.GetProperty
                                | BindingFlags.Public
                                | BindingFlags.Instance)
                            ?? throw new InvalidOperationException("PropertyInfo not found: UnityEditor.MaterialPropertyHandler.propertyDrawer")),
                    typeMtud)),
            Expression.IfThen(
                Expression.Equal(
                    pMaterialToggleUIDrawer,
                    cNull),
                Expression.Throw(
                    Expression.New(
                        ciArgumentException,
                        Expression.Constant("Specified MaterialProperty does not have UnityEditor.MaterialToggleUIDrawer")))),
            Expression.Call(
                pMaterialToggleUIDrawer,
                typeMtud.GetMethod(
                    "SetKeyword",
                    BindingFlags.NonPublic
                        | BindingFlags.Instance)
                    ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialToggleUIDrawer.SetKeyword"),
                pMaterialProperty,
                pBool)),
        "SetKeyword",
        new []
        {
            pShader,
            pMaterialProperty,
            pBool
        }).Compile();
}

上記の SetToggleKeyword() メソッドを利用して下記のように記述する.

protected override void DrawCustomProperties(Material material)
{
    // ...

    using (new EditorGUILayout.VerticalScope(boxOuter))
    {
        DrawToggleLeft(material, _toggleProp, GetLoc("sToggleProp"));
        if (_enableWorldPos.floatValue >= 0.5f)
        {
            // 関連するプロパティの描画
        }
    }

    // ...
}

/// <summary>
/// Draw ToggleLeft property.
/// </summary>
/// <param name="material">Target <see cref="Material"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
/// <param name="label">Label for this toggle button.</param>
private static void DrawToggleLeft(Material material, MaterialProperty prop, string label)
{
    using (var ccScope = new EditorGUI.ChangeCheckScope())
    {
        EditorGUI.showMixedValue = prop.hasMixedValue;
        var isChecked = EditorGUI.ToggleLeft(
            EditorGUILayout.GetControlRect(),
            label,
            prop.floatValue >= 0.5f,
            customToggleFont);
        EditorGUI.showMixedValue = false;
        if (ccScope.changed)
        {
            prop.floatValue = isChecked ? 1.0f : 0.0f;
            if (isMulti)
            {
                SetToggleKeyword(material.shader, prop);
            }
        }
    }
}

オリジナルのlilToonからの移行を簡単にする

テンプレートのインスペクタのコード末尾のコメントアウト部分を解除すると,マテリアルの右クリックメニューが追加される. 名前は適切に置きかえること.

[MenuItem("Assets/TemplateFull/Convert material to custom shader", false, 1100)]
private static void ConvertMaterialToCustomShaderMenu()
{
    if(Selection.objects.Length == 0) return;
    TemplateFullInspector inspector = new TemplateFullInspector();
    for(int i = 0; i < Selection.objects.Length; i++)
    {
        if(Selection.objects[i] is Material)
        {
            inspector.ConvertMaterialToCustomShader((Material)Selection.objects[i]);
        }
    }
}

ただし,上記コードはCtrl-Zが考慮されていない,C# のコードとしてイマイチ,inspector.ConvertMaterialToCustomShader の処理が大袈裟である(lilToon本体とカスタムシェーダーの全てのバリエーションについて Shader.Find() を呼び出す)ので,下記のようにするのがオススメである.

/// <summary>
/// Try to replace the shader of the selected material to custom lilToon shader.
/// </summary>
[MenuItem("Assets/TemplateFull/Convert material to custom shader", false, 1100)]
private static void ConvertMaterialToCustomShaderMenu()
{
    foreach (var obj in Selection.objects)
    {
        var material = obj as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingCustomShader(material.shader);
        if (shader == null)
        {
            Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not original lilToon shader.", AssetDatabase.GetAssetPath(material), material.shader.name);
            continue;
        }

        Undo.RecordObject(material, "TemplateFull/ConvertMaterialToCustomShaderMenu");

        var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
        material.shader = shader;
        material.renderQueue = renderQueue;
    }
}

/// <summary>
/// Get a custom lilToon shader which is corresponding to specified original lilToon shader.
/// </summary>
/// <param name="originalShader">Original lilToon shader.</param>
/// <returns>null if no custom lilToon shader is found, otherwise the one found.</returns>
private static Shader GetCorrespondingCustomShader(Shader originalShader)
{
    var customShaderName = GetCorrespondingCustomShaderName(originalShader.name);
    return customShaderName == null ? null : Shader.Find(customShaderName);
}

/// <summary>
/// Get a custom lilToon shader name which is corresponding to specified original lilToon shader name.
/// </summary>
/// <param name="originalShaderName">Original lilToon shader name.</param>
/// <returns>null if no custom lilToon shader name is found, otherwise the one found.</returns>
private static string GetCorrespondingCustomShaderName(string originalShaderName)
{
    switch (originalShaderName)
    {
        case "lilToon": return shaderName + "/lilToon";
        case "Hidden/lilToonCutout": return "Hidden/" + shaderName + "/Cutout";
        case "Hidden/lilToonTransparent": return "Hidden/" + shaderName + "/Transparent";
        case "Hidden/lilToonOnePassTransparent": return "Hidden/" + shaderName + "/OnePassTransparent";
        case "Hidden/lilToonTwoPassTransparent": return "Hidden/" + shaderName + "/TwoPassTransparent";
        case "Hidden/lilToonOutline": return "Hidden/" + shaderName + "/OpaqueOutline";
        case "Hidden/lilToonCutoutOutline": return "Hidden/" + shaderName + "/CutoutOutline";
        case "Hidden/lilToonTransparentOutline": return "Hidden/" + shaderName + "/TransparentOutline";
        case "Hidden/lilToonOnePassTransparentOutline": return "Hidden/" + shaderName + "/OnePassTransparentOutline";
        case "Hidden/lilToonTwoPassTransparentOutline": return "Hidden/" + shaderName + "/TwoPassTransparentOutline";
        case "_lil/[Optional] lilToonOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Opaque";
        case "_lil/[Optional] lilToonCutoutOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Cutout";
        case "_lil/[Optional] lilToonTransparentOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Transparent";
        case "Hidden/lilToonTessellation": return "Hidden/" + shaderName + "/Tessellation/Opaque";
        case "Hidden/lilToonTessellationCutout": return "Hidden/" + shaderName + "/Tessellation/Cutout";
        case "Hidden/lilToonTessellationTransparent": return "Hidden/" + shaderName + "/Tessellation/Transparent";
        case "Hidden/lilToonTessellationOnePassTransparent": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparent";
        case "Hidden/lilToonTessellationTwoPassTransparent": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent";
        case "Hidden/lilToonTessellationOutline": return "Hidden/" + shaderName + "/Tessellation/OpaqueOutline";
        case "Hidden/lilToonTessellationCutoutOutline": return "Hidden/" + shaderName + "/Tessellation/CutoutOutline";
        case "Hidden/lilToonTessellationTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TransparentOutline";
        case "Hidden/lilToonTessellationOnePassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline";
        case "Hidden/lilToonTessellationTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline";
        case "Hidden/lilToonLite": return shaderName + "/lilToonLite";
        case "Hidden/lilToonLiteCutout": return "Hidden/" + shaderName + "/Lite/Cutout";
        case "Hidden/lilToonLiteTransparent": return "Hidden/" + shaderName + "/Lite/Transparent";
        case "Hidden/lilToonLiteOnePassTransparent": return "Hidden/" + shaderName + "/Lite/OnePassTransparent";
        case "Hidden/lilToonLiteTwoPassTransparent": return "Hidden/" + shaderName + "/Lite/TwoPassTransparent";
        case "Hidden/lilToonLiteOutline": return "Hidden/" + shaderName + "/Lite/OpaqueOutline";
        case "Hidden/lilToonLiteCutoutOutline": return "Hidden/" + shaderName + "/Lite/CutoutOutline";
        case "Hidden/lilToonLiteTransparentOutline": return "Hidden/" + shaderName + "/Lite/TransparentOutline";
        case "Hidden/lilToonLiteOnePassTransparentOutline": return "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline";
        case "Hidden/lilToonLiteTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline";
        case "Hidden/lilToonRefraction": return "Hidden/" + shaderName + "/Refraction";
        case "Hidden/lilToonRefractionBlur": return "Hidden/" + shaderName + "/RefractionBlur";
        case "Hidden/lilToonFur": return "Hidden/" + shaderName + "/Fur";
        case "Hidden/lilToonFurCutout": return "Hidden/" + shaderName + "/FurCutout";
        case "Hidden/lilToonFurTwoPass": return "Hidden/" + shaderName + "/FurTwoPass";
        case "_lil/[Optional] lilToonFurOnly": return shaderName + "/[Optional] FurOnly/Transparent";
        case "_lil/[Optional] lilToonFurOnlyCutout": return shaderName + "/[Optional] FurOnly/Cutout";
        case "_lil/[Optional] lilToonFurOnlyTwoPass": return shaderName + "/[Optional] FurOnly/TwoPass";
        case "Hidden/lilToonGem": return "Hidden/" + shaderName + "/Gem";
        case "_lil/lilToonFakeShadow": return shaderName + "/[Optional] FakeShadow";
        case "_lil/[Optional] lilToonOverlay": return shaderName + "/[Optional] Overlay";
        case "_lil/[Optional] lilToonOverlayOnePass": return shaderName + "/[Optional] OverlayOnePass";
        case "_lil/[Optional] lilToonLiteOverlay": return shaderName + "/[Optional] LiteOverlay";
        case "_lil/[Optional] lilToonLiteOverlayOnePass": return shaderName + "/[Optional] LiteOverlayOnePass";
        case "_lil/lilToonMulti": return shaderName + "/lilToonMulti";
        case "Hidden/lilToonMultiOutline": return "Hidden/" + shaderName + "/MultiOutline";
        case "Hidden/lilToonMultiRefraction": return "Hidden/" + shaderName + "/MultiRefraction";
        case "Hidden/lilToonMultiFur": return "Hidden/" + shaderName + "/MultiFur";
        case "Hidden/lilToonMultiGem": return "Hidden/" + shaderName + "/MultiGem";
        default: return null;
    }
}

テンプレートに従っているならば, shaderName はクラス内で下記のように宣言されているはずであり,これを用いるようにしている.

private const string shaderName = "TemplateFull";

カスタムシェーダーからオリジナルのlilToonへ簡単に戻せるようにする

前述のものと逆の動作を行うメソッドを用意し,右クリックメニューとして登録する.

/// <summary>
/// Try to replace the shader of the material to original lilToon shader.
/// </summary>
[MenuItem("Assets/TemplateFull/Convert material to original shader", false, 1101)]
private static void ConvertMaterialToOriginalShaderMenu()
{
    foreach (var obj in Selection.objects)
    {
        var material = obj as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingOriginalShader(material.shader);
        if (shader == null)
        {
            Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not custom lilToon shader, \"" + shaderName + "\".", AssetDatabase.GetAssetPath(material), material.shader.name);
            continue;
        }

        Undo.RecordObject(material, "TemplateFull/ConvertMaterialToOriginalShaderMenu");

        var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
        material.shader = shader;
        material.renderQueue = renderQueue;
    }
}

/// <summary>
/// Get a original lilToon shader which is corresponding to specified custom lilToon shader.
/// </summary>
/// <param name="customShader">Custom lilToon shader.</param>
/// <returns>null if no original lilToon shader is found, otherwise the one found.</returns>
private static Shader GetCorrespondingOriginalShader(Shader customShader)
{
    var customShaderName = GetCorrespondingOriginalShaderName(customShader.name);
    return customShaderName == null ? null : Shader.Find(customShaderName);
}

/// <summary>
/// Get a original lilToon shader name which is corresponding to specified custom lilToon shader name.
/// </summary>
/// <param name="customShaderName">Custom lilToon shader name.</param>
/// <returns>null if no original lilToon shader name is found, otherwise the one found.</returns>
private static string GetCorrespondingOriginalShaderName(string customShaderName)
{
    switch (customShaderName)
    {
        case shaderName + "/lilToon": return "lilToon";
        case "Hidden/" + shaderName + "/Cutout": return "Hidden/lilToonCutout";
        case "Hidden/" + shaderName + "/Transparent": return "Hidden/lilToonTransparent";
        case "Hidden/" + shaderName + "/OnePassTransparent": return "Hidden/lilToonOnePassTransparent";
        case "Hidden/" + shaderName + "/TwoPassTransparent": return "Hidden/lilToonTwoPassTransparent";
        case "Hidden/" + shaderName + "/OpaqueOutline": return "Hidden/lilToonOutline";
        case "Hidden/" + shaderName + "/CutoutOutline": return "Hidden/lilToonCutoutOutline";
        case "Hidden/" + shaderName + "/TransparentOutline": return "Hidden/lilToonTransparentOutline";
        case "Hidden/" + shaderName + "/OnePassTransparentOutline": return "Hidden/lilToonOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/TwoPassTransparentOutline": return "Hidden/lilToonTwoPassTransparentOutline";
        case shaderName + "/[Optional] OutlineOnly/Opaque": return "_lil/[Optional] lilToonOutlineOnly";
        case shaderName + "/[Optional] OutlineOnly/Cutout": return "_lil/[Optional] lilToonCutoutOutlineOnly";
        case shaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonTransparentOutlineOnly";
        case "Hidden/" + shaderName + "/Tessellation/Opaque": return "Hidden/lilToonTessellation";
        case "Hidden/" + shaderName + "/Tessellation/Cutout": return "Hidden/lilToonTessellationCutout";
        case "Hidden/" + shaderName + "/Tessellation/Transparent": return "Hidden/lilToonTessellationTransparent";
        case "Hidden/" + shaderName + "/Tessellation/OnePassTransparent": return "Hidden/lilToonTessellationOnePassTransparent";
        case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent": return "Hidden/lilToonTessellationTwoPassTransparent";
        case "Hidden/" + shaderName + "/Tessellation/OpaqueOutline": return "Hidden/lilToonTessellationOutline";
        case "Hidden/" + shaderName + "/Tessellation/CutoutOutline": return "Hidden/lilToonTessellationCutoutOutline";
        case "Hidden/" + shaderName + "/Tessellation/TransparentOutline": return "Hidden/lilToonTessellationTransparentOutline";
        case "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline": return "Hidden/lilToonTessellationOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline": return "Hidden/lilToonTessellationTwoPassTransparentOutline";
        case shaderName + "/lilToonLite": return "Hidden/lilToonLite";
        case "Hidden/" + shaderName + "/Lite/Cutout": return "Hidden/lilToonLiteCutout";
        case "Hidden/" + shaderName + "/Lite/Transparent": return "Hidden/lilToonLiteTransparent";
        case "Hidden/" + shaderName + "/Lite/OnePassTransparent": return "Hidden/lilToonLiteOnePassTransparent";
        case "Hidden/" + shaderName + "/Lite/TwoPassTransparent": return "Hidden/lilToonLiteTwoPassTransparent";
        case "Hidden/" + shaderName + "/Lite/OpaqueOutline": return "Hidden/lilToonLiteOutline";
        case "Hidden/" + shaderName + "/Lite/CutoutOutline": return "Hidden/lilToonLiteCutoutOutline";
        case "Hidden/" + shaderName + "/Lite/TransparentOutline": return "Hidden/lilToonLiteTransparentOutline";
        case "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline": return "Hidden/lilToonLiteOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline": return "Hidden/lilToonLiteTwoPassTransparentOutline";
        case "Hidden/" + shaderName + "/Refraction": return "Hidden/lilToonRefraction";
        case "Hidden/" + shaderName + "/RefractionBlur": return "Hidden/lilToonRefractionBlur";
        case "Hidden/" + shaderName + "/Fur": return "Hidden/lilToonFur";
        case "Hidden/" + shaderName + "/FurCutout": return "Hidden/lilToonFurCutout";
        case "Hidden/" + shaderName + "/FurTwoPass": return "Hidden/lilToonFurTwoPass";
        case shaderName + "/[Optional] FurOnly/Transparent": return "_lil/[Optional] lilToonFurOnly";
        case shaderName + "/[Optional] FurOnly/Cutout": return "_lil/[Optional] lilToonFurOnlyCutout";
        case shaderName + "/[Optional] FurOnly/TwoPass": return "_lil/[Optional] lilToonFurOnlyTwoPass";
        case "Hidden/" + shaderName + "/Gem": return "Hidden/lilToonGem";
        case shaderName + "/[Optional] FakeShadow": return "_lil/lilToonFakeShadow";
        case shaderName + "/[Optional] Overlay": return "_lil/[Optional] lilToonOverlay";
        case shaderName + "/[Optional] OverlayOnePass": return "_lil/[Optional] lilToonOverlayOnePass";
        case shaderName + "/[Optional] LiteOverlay": return "_lil/[Optional] lilToonLiteOverlay";
        case shaderName + "/[Optional] LiteOverlayOnePass": return "_lil/[Optional] lilToonLiteOverlayOnePass";
        case shaderName + "/lilToonMulti": return "_lil/lilToonMulti";
        case "Hidden/" + shaderName + "/MultiOutline": return "Hidden/lilToonMultiOutline";
        case "Hidden/" + shaderName + "/MultiRefraction": return "Hidden/lilToonMultiRefraction";
        case "Hidden/" + shaderName + "/MultiFur": return "Hidden/lilToonMultiFur";
        case "Hidden/" + shaderName + "/MultiGem": return "Hidden/lilToonMultiGem";
        default: return null;
    }
}

shaderNameconst string であるため,文字列リテラルとの結合結果もまたコンパイル時定数となり,caseのラベルとして使用できる.

その他

本体のソースコードリーディングを楽にする

Vim等のctagsに対応しているエディタを使っている人向け. lilToon本体のソースコードGitHubからクローンし,タグファイルを作成しておくと,本体の関数やマクロへの定義ジャンプが可能になり,色々と楽になる.

以下は ~/github/lilToon に本体のソースコードをcloneしたものとしたタグファイル(フルパスのタグファイル)の生成方法である.

  • シェーダーファイル向け
$ ctags -f shaderlab.lilToon.tags --languages=c --langmap=c:+.shader,c:+.hlsl -R ~/lilToon/Assets/lilToon/Shader/
$ ctags -f cs.lilToon.tags --languages=c# -R ~/lilToon/Assets/lilToon/

上記のタグファイルを ~/.vim/tagfiles/ に配置し,~/.vimrc に下記のように記述するとよい.

augroup MyCtags
  autocmd!
  autocmd FileType hlsl,shaderlab setlocal tags+=~/.vim/tagfiles/shaderlab.lilToon.tags
  autocmd FileType cs setlocal tags+=~/.vim/tagfiles/win32unix/cs.lilToon.tags
augroup END

デフォルトだとファイルタイプがhlsl,shaderlabの判定がされないため,下記2ファイルを用意する必要がある. 別途プラグインを導入して判別できている場合はしなくてよい.

  • ~/.vim/ftdetect/hlsl.vim
au BufNewFile,BufRead *.hlsl setfiletype shaderlab
  • ~/.vim/ftdetect/shaderlab.vim
au BufNewFile,BufRead *.shaderlab setfiletype shaderlab

記事内容全部入りのインスペクタテンプレート

記事内容+自分好みに修正したインスペクタテンプレート. 全部入りのテンプレートのunitypackageは ここから ダウンロードできる. また,GitHubリポジトリとしても置いてある

長いので折り畳み(クリックで開く閉じる).

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;


namespace lilToon
{
    /// <summary>
    /// <see cref="ShaderGUI"/> for the custom shader variations of lilToon.
    /// </summary>
    public class TemplateAllInspector : lilToonInspector
    {
        // Custom properties
        //private MaterialProperty customVariable;

        /// <summary>
        /// A flag whether to fold custom properties or not.
        /// </summary>
        private static bool isShowCustomProperties;

        /// <summary>
        /// GUID of shader directory.
        /// </summary>
        // TODO: Replace to GUID of your "Shader" directory.
        public const string GuidShaderDir = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
        /// <summary>
        /// Name of this custom shader.
        /// </summary>
        private const string ShaderName = "TemplateAll";

        /// <summary>
        /// Load custom language file and make cache of shader properties.
        /// </summary>
        /// <param name="props">Properties of the material.</param>
        /// <param name="material">Target material.</param>
        protected override void LoadCustomProperties(MaterialProperty[] props, Material material)
        {
            isCustomShader = true;

            // If you want to change rendering modes in the editor, specify the shader here
            ReplaceToCustomShaders();
            isShowRenderMode = !material.shader.name.Contains("/[Optional] ");

            // If not, set isShowRenderMode to false
            //isShowRenderMode = false;

            //LoadCustomLanguage("");  // TODO: Specify GUID of your lang_custom.txt
            //customVariable = FindProperty("_CustomVariable", props);
        }

        /// <summary>
        /// Draw custom properties.
        /// </summary>
        /// <param name="material">Target material.</param>
        protected override void DrawCustomProperties(Material material)
        {
            // GUIStyles Name   Description
            // ---------------- ------------------------------------
            // boxOuter         outer box
            // boxInnerHalf     inner box
            // boxInner         inner box without label
            // customBox        box (similar to unity default box)
            // customToggleFont label for box

            var titleLoc = GetLoc("sCustomShaderTitle");
            isShowCustomProperties = Foldout(titleLoc, titleLoc, isShowCustomProperties);
            if (!isShowCustomProperties)
            {
                return;
            }

            using (new EditorGUILayout.VerticalScope(boxOuter))
            {
                EditorGUILayout.LabelField(GetLoc("sCustomPropertyCategory"), customToggleFont);
                using (new EditorGUILayout.VerticalScope(boxInnerHalf))
                {
                    //m_MaterialEditor.ShaderProperty(customVariable, "Custom Variable");
                }
            }
        }

        /// <summary>
        /// Replace shaders to custom shaders.
        /// </summary>
        protected override void ReplaceToCustomShaders()
        {
            lts         = Shader.Find(ShaderName + "/lilToon");
            ltsc        = Shader.Find("Hidden/" + ShaderName + "/Cutout");
            ltst        = Shader.Find("Hidden/" + ShaderName + "/Transparent");
            ltsot       = Shader.Find("Hidden/" + ShaderName + "/OnePassTransparent");
            ltstt       = Shader.Find("Hidden/" + ShaderName + "/TwoPassTransparent");

            ltso        = Shader.Find("Hidden/" + ShaderName + "/OpaqueOutline");
            ltsco       = Shader.Find("Hidden/" + ShaderName + "/CutoutOutline");
            ltsto       = Shader.Find("Hidden/" + ShaderName + "/TransparentOutline");
            ltsoto      = Shader.Find("Hidden/" + ShaderName + "/OnePassTransparentOutline");
            ltstto      = Shader.Find("Hidden/" + ShaderName + "/TwoPassTransparentOutline");

            ltsoo       = Shader.Find(ShaderName + "/[Optional] OutlineOnly/Opaque");
            ltscoo      = Shader.Find(ShaderName + "/[Optional] OutlineOnly/Cutout");
            ltstoo      = Shader.Find(ShaderName + "/[Optional] OutlineOnly/Transparent");

            ltstess     = Shader.Find("Hidden/" + ShaderName + "/Tessellation/Opaque");
            ltstessc    = Shader.Find("Hidden/" + ShaderName + "/Tessellation/Cutout");
            ltstesst    = Shader.Find("Hidden/" + ShaderName + "/Tessellation/Transparent");
            ltstessot   = Shader.Find("Hidden/" + ShaderName + "/Tessellation/OnePassTransparent");
            ltstesstt   = Shader.Find("Hidden/" + ShaderName + "/Tessellation/TwoPassTransparent");

            ltstesso    = Shader.Find("Hidden/" + ShaderName + "/Tessellation/OpaqueOutline");
            ltstessco   = Shader.Find("Hidden/" + ShaderName + "/Tessellation/CutoutOutline");
            ltstessto   = Shader.Find("Hidden/" + ShaderName + "/Tessellation/TransparentOutline");
            ltstessoto  = Shader.Find("Hidden/" + ShaderName + "/Tessellation/OnePassTransparentOutline");
            ltstesstto  = Shader.Find("Hidden/" + ShaderName + "/Tessellation/TwoPassTransparentOutline");

            ltsl        = Shader.Find(ShaderName + "/lilToonLite");
            ltslc       = Shader.Find("Hidden/" + ShaderName + "/Lite/Cutout");
            ltslt       = Shader.Find("Hidden/" + ShaderName + "/Lite/Transparent");
            ltslot      = Shader.Find("Hidden/" + ShaderName + "/Lite/OnePassTransparent");
            ltsltt      = Shader.Find("Hidden/" + ShaderName + "/Lite/TwoPassTransparent");

            ltslo       = Shader.Find("Hidden/" + ShaderName + "/Lite/OpaqueOutline");
            ltslco      = Shader.Find("Hidden/" + ShaderName + "/Lite/CutoutOutline");
            ltslto      = Shader.Find("Hidden/" + ShaderName + "/Lite/TransparentOutline");
            ltsloto     = Shader.Find("Hidden/" + ShaderName + "/Lite/OnePassTransparentOutline");
            ltsltto     = Shader.Find("Hidden/" + ShaderName + "/Lite/TwoPassTransparentOutline");

            ltsref      = Shader.Find("Hidden/" + ShaderName + "/Refraction");
            ltsrefb     = Shader.Find("Hidden/" + ShaderName + "/RefractionBlur");
            ltsfur      = Shader.Find("Hidden/" + ShaderName + "/Fur");
            ltsfurc     = Shader.Find("Hidden/" + ShaderName + "/FurCutout");
            ltsfurtwo   = Shader.Find("Hidden/" + ShaderName + "/FurTwoPass");
            ltsfuro     = Shader.Find(ShaderName + "/[Optional] FurOnly/Transparent");
            ltsfuroc    = Shader.Find(ShaderName + "/[Optional] FurOnly/Cutout");
            ltsfurotwo  = Shader.Find(ShaderName + "/[Optional] FurOnly/TwoPass");
            ltsgem      = Shader.Find("Hidden/" + ShaderName + "/Gem");
            ltsfs       = Shader.Find(ShaderName + "/[Optional] FakeShadow");

            ltsover     = Shader.Find(ShaderName + "/[Optional] Overlay");
            ltsoover    = Shader.Find(ShaderName + "/[Optional] OverlayOnePass");
            ltslover    = Shader.Find(ShaderName + "/[Optional] LiteOverlay");
            ltsloover   = Shader.Find(ShaderName + "/[Optional] LiteOverlayOnePass");

            ltsm        = Shader.Find(ShaderName + "/lilToonMulti");
            ltsmo       = Shader.Find("Hidden/" + ShaderName + "/MultiOutline");
            ltsmref     = Shader.Find("Hidden/" + ShaderName + "/MultiRefraction");
            ltsmfur     = Shader.Find("Hidden/" + ShaderName + "/MultiFur");
            ltsmgem     = Shader.Find("Hidden/" + ShaderName + "/MultiGem");
        }


        /// <summary>
        /// Try to replace the shader of the selected material to custom lilToon shader.
        /// </summary>
        [MenuItem("Assets/" + ShaderName + "/Convert material to custom shader", false, 1100)]
        private static void ConvertMaterialToCustomShaderMenu()
        {
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                var shader = GetCorrespondingCustomShader(material.shader);
                if (shader == null)
                {
                    Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not original lilToon shader.", AssetDatabase.GetAssetPath(material), material.shader.name);
                    continue;
                }

                Undo.RecordObject(material, ShaderName + "/ConvertMaterialToCustomShaderMenu");

                var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
                material.shader = shader;
                material.renderQueue = renderQueue;
            }
        }

        /// <summary>
        /// Menu validation method for <see cref="ConvertMaterialToCustomShaderMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="ConvertMaterialToCustomShaderMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + ShaderName + "/Convert material to custom shader", true)]
        private static bool ValidateConvertMaterialToCustomShaderMenu()
        {
            var count = 0;
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                if (GetCorrespondingCustomShaderName(material.shader.name) != null)
                {
                    count++;
                }
            }
            return count > 0;
        }

        /// <summary>
        /// Try to replace the shader of the material to original lilToon shader.
        /// </summary>
        [MenuItem("Assets/" + ShaderName + "/Convert material to original shader", false, 1101)]
        private static void ConvertMaterialToOriginalShaderMenu()
        {
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                var shader = GetCorrespondingOriginalShader(material.shader);
                if (shader == null)
                {
                    Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not custom lilToon shader, \"" + ShaderName + "\".", AssetDatabase.GetAssetPath(material), material.shader.name);
                    continue;
                }

                Undo.RecordObject(material, ShaderName + "/ConvertMaterialToOriginalShaderMenu");

                var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
                material.shader = shader;
                material.renderQueue = renderQueue;
            }
        }

        /// <summary>
        /// Menu validation method for <see cref="ValidateConvertMaterialToOriginalShaderMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="ValidateConvertMaterialToOriginalShaderMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + ShaderName + "/Convert material to original shader", true)]
        private static bool ValidateConvertMaterialToOriginalShader()
        {
            var count = 0;
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                if (GetCorrespondingOriginalShaderName(material.shader.name) != null)
                {
                    count++;
                }
            }
            return count > 0;
        }

        /// <summary>
        /// Callback method for menu item which refreshes shader cache and reimport.
        /// </summary>
        [MenuItem("Assets/" + ShaderName + "/Refresh shader cache", false, 2000)]
        private static void RefreshShaderCacheMenu()
        {
            var result = NativeMethods.Open("Library/ShaderCache.db", out var dbHandle);
            if (result != 0)
            {
                Debug.LogError($"Failed to open Library/ShaderCache.db [{result}]");
                return;
            }

            try
            {
                result = NativeMethods.Execute(dbHandle, "DELETE FROM shadererrors", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
                if (result != 0)
                {
                    Debug.LogError($"SQL failed [{result}]");
                    return;
                }
            }
            finally
            {
                result = NativeMethods.Close(dbHandle);
                if (result != 0)
                {
                    Debug.LogError($"Failed to close database [{result}]");
                }
            }

            var shaderDirPath = AssetDatabase.GUIDToAssetPath(GuidShaderDir);
            if (shaderDirPath == "")
            {
                Debug.LogWarning("Cannot find file or directory corresponding to GUID: " + GuidShaderDir);
                return;
            }
            if (!Directory.Exists(shaderDirPath))
            {
                Debug.LogWarningFormat("Directory not found: {0} ({1})", shaderDirPath, GuidShaderDir);
                return;
            }
            AssetDatabase.ImportAsset(shaderDirPath, ImportAssetOptions.ImportRecursive);
        }

        /// <summary>
        /// Menu validation method for <see cref="RefreshShaderCacheMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="RefreshShaderCacheMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + ShaderName + "Refresh shader cache", true)]
        private static bool ValidateRefreshShaderCacheMenu()
        {
            try
            {
                NativeMethods.Close(IntPtr.Zero);
                return true;
            }
            catch (DllNotFoundException)
            {
                return false;
            }
        }

        /// <summary>
        /// Get a custom lilToon shader which is corresponding to specified original lilToon shader.
        /// </summary>
        /// <param name="originalShader">Original lilToon shader.</param>
        /// <returns>null if no custom lilToon shader is found, otherwise the one found.</returns>
        private static Shader GetCorrespondingCustomShader(Shader originalShader)
        {
            var customShaderName = GetCorrespondingCustomShaderName(originalShader.name);
            return customShaderName == null ? null : Shader.Find(customShaderName);
        }

        /// <summary>
        /// Get a custom lilToon shader name which is corresponding to specified original lilToon shader name.
        /// </summary>
        /// <param name="originalShaderName">Original lilToon shader name.</param>
        /// <returns>null if no custom lilToon shader name is found, otherwise the one found.</returns>
        private static string GetCorrespondingCustomShaderName(string originalShaderName)
        {
            switch (originalShaderName)
            {
                case "lilToon": return ShaderName + "/lilToon";
                case "Hidden/lilToonCutout": return "Hidden/" + ShaderName + "/Cutout";
                case "Hidden/lilToonTransparent": return "Hidden/" + ShaderName + "/Transparent";
                case "Hidden/lilToonOnePassTransparent": return "Hidden/" + ShaderName + "/OnePassTransparent";
                case "Hidden/lilToonTwoPassTransparent": return "Hidden/" + ShaderName + "/TwoPassTransparent";
                case "Hidden/lilToonOutline": return "Hidden/" + ShaderName + "/OpaqueOutline";
                case "Hidden/lilToonCutoutOutline": return "Hidden/" + ShaderName + "/CutoutOutline";
                case "Hidden/lilToonTransparentOutline": return "Hidden/" + ShaderName + "/TransparentOutline";
                case "Hidden/lilToonOnePassTransparentOutline": return "Hidden/" + ShaderName + "/OnePassTransparentOutline";
                case "Hidden/lilToonTwoPassTransparentOutline": return "Hidden/" + ShaderName + "/TwoPassTransparentOutline";
                case "_lil/[Optional] lilToonOutlineOnly": return ShaderName + "/[Optional] OutlineOnly/Opaque";
                case "_lil/[Optional] lilToonCutoutOutlineOnly": return ShaderName + "/[Optional] OutlineOnly/Cutout";
                case "_lil/[Optional] lilToonTransparentOutlineOnly": return ShaderName + "/[Optional] OutlineOnly/Transparent";
                case "Hidden/lilToonTessellation": return "Hidden/" + ShaderName + "/Tessellation/Opaque";
                case "Hidden/lilToonTessellationCutout": return "Hidden/" + ShaderName + "/Tessellation/Cutout";
                case "Hidden/lilToonTessellationTransparent": return "Hidden/" + ShaderName + "/Tessellation/Transparent";
                case "Hidden/lilToonTessellationOnePassTransparent": return "Hidden/" + ShaderName + "/Tessellation/OnePassTransparent";
                case "Hidden/lilToonTessellationTwoPassTransparent": return "Hidden/" + ShaderName + "/Tessellation/TwoPassTransparent";
                case "Hidden/lilToonTessellationOutline": return "Hidden/" + ShaderName + "/Tessellation/OpaqueOutline";
                case "Hidden/lilToonTessellationCutoutOutline": return "Hidden/" + ShaderName + "/Tessellation/CutoutOutline";
                case "Hidden/lilToonTessellationTransparentOutline": return "Hidden/" + ShaderName + "/Tessellation/TransparentOutline";
                case "Hidden/lilToonTessellationOnePassTransparentOutline": return "Hidden/" + ShaderName + "/Tessellation/OnePassTransparentOutline";
                case "Hidden/lilToonTessellationTwoPassTransparentOutline": return "Hidden/" + ShaderName + "/Tessellation/TwoPassTransparentOutline";
                case "Hidden/lilToonLite": return ShaderName + "/lilToonLite";
                case "Hidden/lilToonLiteCutout": return "Hidden/" + ShaderName + "/Lite/Cutout";
                case "Hidden/lilToonLiteTransparent": return "Hidden/" + ShaderName + "/Lite/Transparent";
                case "Hidden/lilToonLiteOnePassTransparent": return "Hidden/" + ShaderName + "/Lite/OnePassTransparent";
                case "Hidden/lilToonLiteTwoPassTransparent": return "Hidden/" + ShaderName + "/Lite/TwoPassTransparent";
                case "Hidden/lilToonLiteOutline": return "Hidden/" + ShaderName + "/Lite/OpaqueOutline";
                case "Hidden/lilToonLiteCutoutOutline": return "Hidden/" + ShaderName + "/Lite/CutoutOutline";
                case "Hidden/lilToonLiteTransparentOutline": return "Hidden/" + ShaderName + "/Lite/TransparentOutline";
                case "Hidden/lilToonLiteOnePassTransparentOutline": return "Hidden/" + ShaderName + "/Lite/OnePassTransparentOutline";
                case "Hidden/lilToonLiteTwoPassTransparentOutline": return "Hidden/" + ShaderName + "/Lite/TwoPassTransparentOutline";
                case "Hidden/lilToonRefraction": return "Hidden/" + ShaderName + "/Refraction";
                case "Hidden/lilToonRefractionBlur": return "Hidden/" + ShaderName + "/RefractionBlur";
                case "Hidden/lilToonFur": return "Hidden/" + ShaderName + "/Fur";
                case "Hidden/lilToonFurCutout": return "Hidden/" + ShaderName + "/FurCutout";
                case "Hidden/lilToonFurTwoPass": return "Hidden/" + ShaderName + "/FurTwoPass";
                case "_lil/[Optional] lilToonFurOnly": return ShaderName + "/[Optional] FurOnly/Transparent";
                case "_lil/[Optional] lilToonFurOnlyCutout": return ShaderName + "/[Optional] FurOnly/Cutout";
                case "_lil/[Optional] lilToonFurOnlyTwoPass": return ShaderName + "/[Optional] FurOnly/TwoPass";
                case "Hidden/lilToonGem": return "Hidden/" + ShaderName + "/Gem";
                case "_lil/lilToonFakeShadow": return ShaderName + "/[Optional] FakeShadow";
                case "_lil/[Optional] lilToonOverlay": return ShaderName + "/[Optional] Overlay";
                case "_lil/[Optional] lilToonOverlayOnePass": return ShaderName + "/[Optional] OverlayOnePass";
                case "_lil/[Optional] lilToonLiteOverlay": return ShaderName + "/[Optional] LiteOverlay";
                case "_lil/[Optional] lilToonLiteOverlayOnePass": return ShaderName + "/[Optional] LiteOverlayOnePass";
                case "_lil/lilToonMulti": return ShaderName + "/lilToonMulti";
                case "Hidden/lilToonMultiOutline": return "Hidden/" + ShaderName + "/MultiOutline";
                case "Hidden/lilToonMultiRefraction": return "Hidden/" + ShaderName + "/MultiRefraction";
                case "Hidden/lilToonMultiFur": return "Hidden/" + ShaderName + "/MultiFur";
                case "Hidden/lilToonMultiGem": return "Hidden/" + ShaderName + "/MultiGem";
                default: return null;
            }
        }

        /// <summary>
        /// Get a original lilToon shader which is corresponding to specified custom lilToon shader.
        /// </summary>
        /// <param name="customShader">Custom lilToon shader.</param>
        /// <returns>null if no original lilToon shader is found, otherwise the one found.</returns>
        private static Shader GetCorrespondingOriginalShader(Shader customShader)
        {
            var customShaderName = GetCorrespondingOriginalShaderName(customShader.name);
            return customShaderName == null ? null : Shader.Find(customShaderName);
        }

        /// <summary>
        /// Get a original lilToon shader name which is corresponding to specified custom lilToon shader name.
        /// </summary>
        /// <param name="customShaderName">Custom lilToon shader name.</param>
        /// <returns>null if no original lilToon shader name is found, otherwise the one found.</returns>
        private static string GetCorrespondingOriginalShaderName(string customShaderName)
        {
            switch (customShaderName)
            {
                case ShaderName + "/lilToon": return "lilToon";
                case "Hidden/" + ShaderName + "/Cutout": return "Hidden/lilToonCutout";
                case "Hidden/" + ShaderName + "/Transparent": return "Hidden/lilToonTransparent";
                case "Hidden/" + ShaderName + "/OnePassTransparent": return "Hidden/lilToonOnePassTransparent";
                case "Hidden/" + ShaderName + "/TwoPassTransparent": return "Hidden/lilToonTwoPassTransparent";
                case "Hidden/" + ShaderName + "/OpaqueOutline": return "Hidden/lilToonOutline";
                case "Hidden/" + ShaderName + "/CutoutOutline": return "Hidden/lilToonCutoutOutline";
                case "Hidden/" + ShaderName + "/TransparentOutline": return "Hidden/lilToonTransparentOutline";
                case "Hidden/" + ShaderName + "/OnePassTransparentOutline": return "Hidden/lilToonOnePassTransparentOutline";
                case "Hidden/" + ShaderName + "/TwoPassTransparentOutline": return "Hidden/lilToonTwoPassTransparentOutline";
                case ShaderName + "/[Optional] OutlineOnly/Opaque": return "_lil/[Optional] lilToonOutlineOnly";
                case ShaderName + "/[Optional] OutlineOnly/Cutout": return "_lil/[Optional] lilToonCutoutOutlineOnly";
                case ShaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonTransparentOutlineOnly";
                case "Hidden/" + ShaderName + "/Tessellation/Opaque": return "Hidden/lilToonTessellation";
                case "Hidden/" + ShaderName + "/Tessellation/Cutout": return "Hidden/lilToonTessellationCutout";
                case "Hidden/" + ShaderName + "/Tessellation/Transparent": return "Hidden/lilToonTessellationTransparent";
                case "Hidden/" + ShaderName + "/Tessellation/OnePassTransparent": return "Hidden/lilToonTessellationOnePassTransparent";
                case "Hidden/" + ShaderName + "/Tessellation/TwoPassTransparent": return "Hidden/lilToonTessellationTwoPassTransparent";
                case "Hidden/" + ShaderName + "/Tessellation/OpaqueOutline": return "Hidden/lilToonTessellationOutline";
                case "Hidden/" + ShaderName + "/Tessellation/CutoutOutline": return "Hidden/lilToonTessellationCutoutOutline";
                case "Hidden/" + ShaderName + "/Tessellation/TransparentOutline": return "Hidden/lilToonTessellationTransparentOutline";
                case "Hidden/" + ShaderName + "/Tessellation/OnePassTransparentOutline": return "Hidden/lilToonTessellationOnePassTransparentOutline";
                case "Hidden/" + ShaderName + "/Tessellation/TwoPassTransparentOutline": return "Hidden/lilToonTessellationTwoPassTransparentOutline";
                case ShaderName + "/lilToonLite": return "Hidden/lilToonLite";
                case "Hidden/" + ShaderName + "/Lite/Cutout": return "Hidden/lilToonLiteCutout";
                case "Hidden/" + ShaderName + "/Lite/Transparent": return "Hidden/lilToonLiteTransparent";
                case "Hidden/" + ShaderName + "/Lite/OnePassTransparent": return "Hidden/lilToonLiteOnePassTransparent";
                case "Hidden/" + ShaderName + "/Lite/TwoPassTransparent": return "Hidden/lilToonLiteTwoPassTransparent";
                case "Hidden/" + ShaderName + "/Lite/OpaqueOutline": return "Hidden/lilToonLiteOutline";
                case "Hidden/" + ShaderName + "/Lite/CutoutOutline": return "Hidden/lilToonLiteCutoutOutline";
                case "Hidden/" + ShaderName + "/Lite/TransparentOutline": return "Hidden/lilToonLiteTransparentOutline";
                case "Hidden/" + ShaderName + "/Lite/OnePassTransparentOutline": return "Hidden/lilToonLiteOnePassTransparentOutline";
                case "Hidden/" + ShaderName + "/Lite/TwoPassTransparentOutline": return "Hidden/lilToonLiteTwoPassTransparentOutline";
                case "Hidden/" + ShaderName + "/Refraction": return "Hidden/lilToonRefraction";
                case "Hidden/" + ShaderName + "/RefractionBlur": return "Hidden/lilToonRefractionBlur";
                case "Hidden/" + ShaderName + "/Fur": return "Hidden/lilToonFur";
                case "Hidden/" + ShaderName + "/FurCutout": return "Hidden/lilToonFurCutout";
                case "Hidden/" + ShaderName + "/FurTwoPass": return "Hidden/lilToonFurTwoPass";
                case ShaderName + "/[Optional] FurOnly/Transparent": return "_lil/[Optional] lilToonFurOnly";
                case ShaderName + "/[Optional] FurOnly/Cutout": return "_lil/[Optional] lilToonFurOnlyCutout";
                case ShaderName + "/[Optional] FurOnly/TwoPass": return "_lil/[Optional] lilToonFurOnlyTwoPass";
                case "Hidden/" + ShaderName + "/Gem": return "Hidden/lilToonGem";
                case ShaderName + "/[Optional] FakeShadow": return "_lil/lilToonFakeShadow";
                case ShaderName + "/[Optional] Overlay": return "_lil/[Optional] lilToonOverlay";
                case ShaderName + "/[Optional] OverlayOnePass": return "_lil/[Optional] lilToonOverlayOnePass";
                case ShaderName + "/[Optional] LiteOverlay": return "_lil/[Optional] lilToonLiteOverlay";
                case ShaderName + "/[Optional] LiteOverlayOnePass": return "_lil/[Optional] lilToonLiteOverlayOnePass";
                case ShaderName + "/lilToonMulti": return "_lil/lilToonMulti";
                case "Hidden/" + ShaderName + "/MultiOutline": return "Hidden/lilToonMultiOutline";
                case "Hidden/" + ShaderName + "/MultiRefraction": return "Hidden/lilToonMultiRefraction";
                case "Hidden/" + ShaderName + "/MultiFur": return "Hidden/lilToonMultiFur";
                case "Hidden/" + ShaderName + "/MultiGem": return "Hidden/lilToonMultiGem";
                default: return null;
            }
        }


        /// <summary>
        /// Provides some native methods of SQLite3.
        /// </summary>
        internal static class NativeMethods
        {
#if UNITY_EDITOR && !UNITY_EDITOR_WIN
            /// <summary>
            /// Native library name of SQLite3.
            /// </summary>
            private const string LibraryName = "sqlite3";
            /// <summary>
            /// Calling convention of library functions.
            /// </summary>
            private const CallingConvention CallConv = CallingConvention.Cdecl;
#else
            /// <summary>
            /// Native library name of SQLite3.
            /// </summary>
            private const string LibraryName = "winsqlite3";
            /// <summary>
            /// Calling convention of library functions.
            /// </summary>
            private const CallingConvention CallConv = CallingConvention.StdCall;
#endif
            /// <summary>
            /// Open database.
            /// </summary>
            /// <param name="filePath">SQLite3 database file path.</param>
            /// <param name="db">SQLite db handle.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_open", CallingConvention = CallConv)]
            public static extern int Open(string filename, out IntPtr dbHandle);

            /// <summary>
            /// Close database.
            /// </summary>
            /// <param name="filePath">Database filename.</param>
            /// <param name="db">SQLite db handle.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/close.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_close", CallingConvention = CallConv)]
            public static extern int Close(IntPtr db);

            /// <summary>
            /// Execute specified SQL.
            /// </summary>
            /// <param name="db">An open database.</param>
            /// <param name="sql">SQL to be evaluated.</param>
            /// <param name="callback">Callback function.</param>
            /// <param name="callbackArg">1st argument to callback.</param>
            /// <param name="pErrMsg">Error msg written here.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/exec.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_exec", CallingConvention = CallConv)]
            public static extern int Execute(IntPtr dbHandle, string sql, IntPtr callback, IntPtr callbackArg, IntPtr pErrMsg);
        }
    }
}

作例