基本的なカスタムシェーダーの情報は下記公式ドキュメントを参照すること.
ドキュメント中にある下記のファイルがテンプレートであり,この中の数ファイルを編集する.
カスタムシェーダーはプリプロセッサを用いて,本体側の特定位置にカスタムシェーダーで記述した処理を差し込んだシェーダーを作成するようになっている. そのため,プリプロセスについて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.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
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);
// }
}
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_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) = 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 を代入することで,頂点に関連するポリゴンを消去するテクニック.
フラグメントシェーダーに渡ってから 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.ShaderProperty
UnityEditor.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のラベルとして使用できる.