基本的なカスタムシェーダーの情報は下記公式ドキュメントを参照すること.
ドキュメント中にある下記のファイルがテンプレートであり,この中の数ファイルを編集する.
カスタムシェーダーはプリプロセッサを用いて,本体側の特定位置にカスタムシェーダーで記述した処理を差し込んだシェーダーを作成するようになっている. そのため,プリプロセスについてC/C++の文献等をあたり,基本的なことは知っておいた方がよい.
基本的には処理置き換えのマクロにとどめておくこと.
関数定義もできるが,インクルード位置がuniform変数の宣言位置(custom.hlsl 内で記述している LIL_CUSTOM_PROPERTIES のマクロの展開箇所も含む)よりも前なので,uniform変数に依存する処理は書けない.
uniform変数に依存する かどうかに関わらず,関数定義は 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 等を参照するとよい.
本体側で TEXCOORD のIDと重複するIDが LIL_CUSTOM_V2F_MEMBER に 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
#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.lilcontainerltsmulti_fur.lilcontainerltsmulti_gem.lilcontainerltsmulti_o.lilcontainerltsmulti_ref.lilcontainer
HLSLINCLUDE
#pragma shader_feature_local _ _TOGGLEPROP_ON
#pragma shader_feature_local _ENUMKEYWORD_FOO _ENUMKEYWORD_BAR _ENUMKEYWORD_BAZ
#include "custom.hlsl"
ENDHLSLlilToonの設計思想に真っ向から対立していると思うが....
まず,前述の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);
// }
}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_AUDIOLINKuniform変数の宣言は custom.hlsl 内の LIL_CUSTOM_PROPERTIES や LIL_CUSTOM_TEXTURES マクロで行うべきと思うかもしれないが,
custom.hlsl の段階では LIL_FEATURE_AUDIOLINK マクロが定義されていないため不可能である.
マクロ LIL_MULTI が定義されているかどうかを調べる.
ただし,このマクロは custom.hlsl の段階では定義されておらず, custom_insert.hlsl の段階でないと使用できないことに注意.
#ifdef LIL_MULTI
// マルチシェーダー用の処理
#endif非multi版でif文を用い,multi版で条件分岐を用いると,同じコードを2度書くことになる.
lilCustomShaderProperties.lilblock
//----------------------------------------------------------------------------------------------------------------------
// Custom Properties
[Toggle] _ToggleProp ("Toggle Property", Int) = 0
[KeywordEnum(Foo, Bar, Baz)] _KeywordEnumProp ("Keyword enum property", Int) = 0custom.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 を代入することで,頂点に関連するポリゴンを消去するテクニック.
フラグメントシェーダーに渡ってから 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.positionCS は float4 であるが, 0.0 / 0.0 は全要素 0.0 / 0.0 の float4 に暗黙的に変換されるのを利用している.
後続の処理は不要なので,returnしておく.
custom.hlsl で下記のように4008番の警告を無効化した箇所で NaN の定数を宣言し,それを用いるようにする.
UNITY_COMPILER_HLSL は HLSLSupport.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");
// ...
}シェーダー側で [Toggle] を指定しているプロパティ(MaterialToggleDrawer)について,lilToon本体の折り畳みに合わせ,なおかつキーワードを定義したい場合の解決法.
(当たり前のことではあるが,Drawerを定義して,そのDrawerを指定する方が良いとは思う.)
下記のように記載した場合, EditorGUI.ToggleLeft が使用されないため不恰好になる.
m_MaterialEditor.ShaderProperty(_toggleProp, "Label for toggle property");UnityEditor.MaterialEditor.ShaderPropertyUnityEditor.MaterialEditor.ShaderPropertyInternal
しかし,UnityEditor.MaterialEditor.ShaderProperty() で行われている処理である
MaterialProperty に設定されている Drawer を取得し,その Drawer の OnGUI() を呼び出すのは,
使用されているクラス・メソッド類が外部からは 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);
}
}
}
}テンプレートのインスペクタのコード末尾のコメントアウト部分を解除すると,マテリアルの右クリックメニューが追加される. 名前は適切に置きかえること.
[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";前述のものと逆の動作を行うメソッドを用意し,右クリックメニューとして登録する.
/// <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;
}
}shaderName は const string であるため,文字列リテラルとの結合結果もまたコンパイル時定数となり,caseのラベルとして使用できる.