Skip to content

Instantly share code, notes, and snippets.

@koturn
Last active August 23, 2023 05:33
Show Gist options
  • Save koturn/030d0a4ed4225a4a46a9ebd8eda629a4 to your computer and use it in GitHub Desktop.
Save koturn/030d0a4ed4225a4a46a9ebd8eda629a4 to your computer and use it in GitHub Desktop.
lilToonのカスタムシェーダーで躓いたり本体コード読んだときのメモ
<style> details { /* font: 16px "Open Sans", Calibri, sans-serif; */ /* width: 620px; */ } details > summary { padding: 2px 6px; /* width: 15em; */ background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; } details[open] > summary { background-color: #ccf; } </style>

基本

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

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

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

基礎知識

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

custom.hlsl の注意点

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

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を一旦終了させておいた方がよいかも)

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

メンバの追加は 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 として渡されているため,コンパイルエラーとなるパスが存在する. 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
#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;

次のバージョンでは直ってるはず....

シェーダーバリアントを使用する

下記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() で保存しておく.

private List<string> _shaderKeywords = new List<string>();

public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
    base.OnGUI(materialEditor, props);
    RestoreKeywords((Material)materialEditor.target, _shaderKeywords);
    _shaderKeywords.Clear();
}

private static void RestoreKeywords(Material material, List<string> shaderKeywords)
{
    foreach (var shaderKeyword in shaderKeywords)
    {
        material.EnableKeyword(keyword);
    }
}

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

    _shaderKeywords.Add($"_TOGGLEPROP_{(prop.floatValue >= 0.5f ? "ON" : "OFF")}");
    // 雑にやる方法.カスタムシェーダー用のキーワード以外も残りそう
    // foreach (var keyword in material.shaderKeywords)
    // {
    //     _shaderKeywords.Add(keyword);
    // }
}

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 マクロが定義されていないため不可能である.

シェーダー側で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 を入れておくことで,コンパイラの最適化処理により,頂点シェーダーの先頭あたりに上記のコードを記述したのと同一のコードが生成される. LIL_V2F_OUT_BASE.positionCSfloat4 であるが, 0.0 / 0.0 は全要素 0.0 / 0.0float4 に暗黙的に変換されるのを利用している. 後続の処理は不要なので,returnしておく.

NaNの警告回避

custom.hlsl で下記のように4008番の警告を無効化した箇所で NaN の定数を宣言し,それを用いるようにする. UNITY_COMPILER_HLSLHLSLSupport.cginc で定義されるマクロなので,一応定義されていない場合の判断も加えている.

#if defined(UNITY_COMPILER_HLSL) \
    || defined(SHADER_API_GLCORE) \
    || defined(SHADER_API_GLES3) \
    || defined(SHADER_API_METAL) \
    || defined(SHADER_API_VULKAN) \
    || defined(SHADER_API_GLES) \
    || defined(SHADER_API_D3D11)
#    pragma warning (disable : 4008)
#endif
static const float kNaN = 0.0 / 0.0;  // NaN
#if defined(UNITY_COMPILER_HLSL) \
    || defined(SHADER_API_GLCORE) \
    || defined(SHADER_API_GLES3) \
    || defined(SHADER_API_METAL) \
    || defined(SHADER_API_VULKAN) \
    || defined(SHADER_API_GLES) \
    || defined(SHADER_API_D3D11)
#    pragma warning (default : 4008)
#endif

#define LIL_CUSTOM_VERT_COPY
    if (_VRCCameraMode == 0) { \
        LIL_INITIALIZE_STRUCT(v2f, LIL_V2F_OUT_BASE); \
        LIL_V2F_OUT_BASE.positionCS = float4(kNaN, kNaN, kNaN, kNaN); \
        return LIL_V2F_OUT; \
    }

マテリアルエディタ

キーワードについて

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

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

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.ExpressionTree;
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, 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()
{
    var objects = Selection.objects;
    if (objects.Length == 0)
    {
        return;
    }

    for (int i = 0; i < objects.Length; i++)
    {
        var material = objects[i] as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingCustomShader(material.shader);
        if (shader == null)
        {
            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>
public 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()
{
    var objects = Selection.objects;
    if (objects.Length == 0)
    {
        return;
    }

    for (int i = 0; i < objects.Length; i++)
    {
        var material = objects[i] as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingOriginalShader(material.shader);
        if (shader == null)
        {
            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のラベルとして使用できる.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment