项目package,可在官网购买,也可以在taobao购买。
项目运行效果,可参看网址:http://www.jeanmoreno.com/unity/toonycolorspro/
另外一个项目:https://github.com/unity3d-jp/unitychan-crs
项目分析参看:https://blog.csdn.net/candycat1992/article/details/51357077
可在github上download下来,看看。
虽然很早的代码,但是看起来绝对的还不out。
下面章节主要是针对其项目中应用的一些技术:
shader的编写
camera的处理
两个方面进行展开学习与记录。
首先是这个模型上脸部使用的shader:
Toony Colors Pro 2/Examples/Cat Demo/UnityChan/Style 1 Skin
这个脸部主要是使用了两个mesh:
还有一个眼睛:
MaterialPropertyDrawer类的重载
参考网址:https://docs.unity3d.com/ScriptReference/MaterialPropertyDrawer.html
The built-in MaterialPropertyDrawers are: ToggleDrawer, EnumDrawer, KeywordEnumDrawer, PowerSliderDrawer, IntRangeDrawer. In shader code, the “Drawer” suffix of the class name is not written; when Unity searches for the drawer class it adds “Drawer” automatically.
这段话告诉我们重写的子类,命名后面,unity会自动为我们加上Drawer关键字,但是如果名字中已经有了Drawer,那么则不会再加上。
这个还是有点诡异的。
接下来的很大的篇幅我们将讲解的是在Toony Colors Pro 2中的shader编辑器的编写方法,这样也可以在今后跟美术提供很友好的UI。
我们讲解的方法,还是采用的是注释代码,一行一行讲解其作用的方法。
在TCP2_Demo_UnityChan style1 skin这个shader中的最后一行,有一个自定义的shader gui显示类:
CustomEditor “TCP2_MaterialInspector_SG”
关键字CustomEditor 用来表面这个shader将会使用自定义的shader显示器。
TCP2_MaterialInspector_SG是显示器的类名称。
下面就看看TCP2_MaterialInspector_SG是如何实现的。
public class TCP2_MaterialInspector_SG : ShaderGUI
{
所有的shader自定义编辑器都继承自ShaderGUI,这是必须的。
然后再看这个类的方法,只有一个方法,就是重载了基类的OnGUI方法:
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
参考例子官网也有:https://docs.unity3d.com/Manual/SL-CustomShaderGUI.html
这里参数:
materialEditor——默认的材质球编辑器,由unity自动给我们传入。
properties——默认的材质球属性数组。
接着
#if SHOW_DEFAULT_INSPECTOR
base.OnGUI();
return;
#else
如果定义了宏SHOW_DEFAULT_INSPECTOR,就使用默认的base.OnGUI方法。
这个我们不care,因为只关心自定义的实现方式,这个可以理解为fallback哈哈。
EditorGUILayout.BeginHorizontal();
var label = (Screen.width > 450f) ?
"TOONY COLORS PRO 2 - INSPECTOR (Generated Shader)" :
(Screen.width > 300f ? "TOONY COLORS PRO 2 - INSPECTOR" : "TOONY COLORS PRO 2");
……
EditorGUILayout.EndHorizontal();
这里使用了编辑器GUI布局,开启了一行的绘制的。
定义了一个label标签,当属性面板,宽度大于450的时候,显示长的字符串:TOONY COLORS PRO 2 - INSPECTOR (Generated Shader)
当属性面板宽度大于300的时候,显示中等长度字符串:TOONY COLORS PRO 2 - INSPECTOR
否则显示短字符串:TOONY COLORS PRO 2
这个也通过控制Inspector面板的宽度来看看其显示效果:
ok,接着看看这个标签如何显示出来:
TCP2_GUI.HeaderBig(label);
它使用了TCP2_GUI类的HeaderBig方法。
此方法如下:
public static void HeaderBig(string header, string tooltip = null)
{
if(tooltip != null)
EditorGUILayout.LabelField(new GUIContent(header, tooltip), BigHeaderLabel);
else
EditorGUILayout.LabelField(header, BigHeaderLabel);
}
传入的是内容字符串和提示字符串。我们这里没有给label添加鼠标移动上去的提示符字符串,所以走的是else语句。
else语句中的BigHeaderLabel,我们看看是什么?
private static GUIStyle _BigHeaderLabel;
private static GUIStyle BigHeaderLabel
{
get
{
if(_BigHeaderLabel == null)
{
_BigHeaderLabel = new GUIStyle(EditorStyles.largeLabel);
_BigHeaderLabel.fontStyle = FontStyle.Bold;
_BigHeaderLabel.fixedHeight = 30;
}
return _BigHeaderLabel;
}
}
它是一个属性,类型为GUIStyle,样式是自定义的样式。定义了字体,高度。绘制结果如下:
ok,一个自定义样式的label绘制完毕。
再次强调下,我们的代码是先把OnGUI里面的方法都注释掉,一行一行打开讲解的,这也是最有效的学习方法。
接着:
if (TCP2_GUI.Button(TCP2_GUI.CogIcon, "O", "Open in Shader Generator"))
{
使用了类TCP2_GUI的Button方法,如下:
public static bool Button(GUIStyle icon, string noIconText, string tooltip = null)
{
if(icon == null)
return GUILayout.Button(new GUIContent(noIconText, tooltip), EditorStyles.miniButton);
return GUILayout.Button(new GUIContent("", tooltip), icon);
}
没有再定义新的类型,都是Unity内置类型。
这样绘制结果是:
这个icon是哪里设置的呢?TCP2_GUI.CogIcon这个定义了icon的样式。
Icon的风格定义如下:
private static GUIStyle _CogIcon;
public static GUIStyle CogIcon
{
get
{
if(_CogIcon == null)
{
_CogIcon = new GUIStyle(EditorStyles.label);
_CogIcon.fixedWidth = 16;
_CogIcon.fixedHeight = 16;
_CogIcon.normal.background = GetCustomTexture("TCP2_CogIcon");
_CogIcon.active.background = GetCustomTexture("TCP2_CogIcon_Down");
}
return _CogIcon;
}
}
首先它是属性,然后如果为null,则new出一个新的style,构造类型为label。设置宽度和高度;背景图片有正常和激活两种。再看:GetCustomTexture方法。
private static Dictionary<string, Texture2D> CustomEditorTextures = new Dictionary<string, Texture2D>();
private static Texture2D GetCustomTexture(string name)
{
var uiName = name + (EditorGUIUtility.isProSkin ? "pro" : "");
if(CustomEditorTextures.ContainsKey(uiName))
return CustomEditorTextures[uiName];
var rootPath = TCP2_Utils.FindReadmePath(true);
Texture2D texture = null;
//load pro version
if(EditorGUIUtility.isProSkin)
texture = AssetDatabase.LoadAssetAtPath("Assets" + rootPath + "/Editor/Icons/" + name + "_Pro.png", typeof(Texture2D)) as Texture2D;
//load default version
if(texture == null)
texture = AssetDatabase.LoadAssetAtPath("Assets" + rootPath + "/Editor/Icons/" + name + ".png", typeof(Texture2D)) as Texture2D;
if(texture != null)
{
CustomEditorTextures.Add(uiName, texture);
return texture;
}
return null;
}
定义了一个字典:CustomEditorTextures,保存加载过的图片。
不用多介绍,它使用的是AssetDatabase.LoadAssetAtPath加载资源的方式。最后把加载的图片加入字典,并返回。
这两个图片长这个样子:
可以看到这个就是上面绘制的图片样子了。
ok。
绘制了button之后,我们肯定会想这个button点击会是什么处理?
if (targetMaterial.shader != null)
{
TCP2_ShaderGenerator.OpenWithShader(targetMaterial.shader);
}
如果目标材质的shader不为空,则使用OpenWithShader方法打开。
这个部分的实现,主要是为了加载shader看看其属性啥的,用处不大,有时间再细细分析。
分隔符的绘制:
TCP2_GUI.Separator();
其实现如下:
public static void Separator()
{
var colorDark = EditorGUIUtility.isProSkin ? new Color(.1f, .1f, .1f) : new Color(.3f, .3f, .3f);
var colorBright = EditorGUIUtility.isProSkin ? new Color(.3f, .3f, .3f) : new Color(.9f, .9f, .9f);
GUILayout.Space(4);
GUILine(colorDark, 1);
GUILine(colorBright, 1);
GUILayout.Space(4);
}
这里的GUILine方法,传入颜色和线的高度使用GUILine方法绘制:
public static void GUILine(Color color, float height = 2f)
{
var position = GUILayoutUtility.GetRect(0f, float.MaxValue, height, height, LineStyle);
if(Event.current.type == EventType.Repaint)
{
var orgColor = GUI.color;
GUI.color = orgColor * color;
LineStyle.Draw(position, false, false, false, false);
GUI.color = orgColor;
}
}
这里线的样式如下:
public static GUIStyle _LineStyle;
public static GUIStyle LineStyle
{
get
{
if(_LineStyle == null)
{
_LineStyle = new GUIStyle();
_LineStyle.normal.background = EditorGUIUtility.whiteTexture;
_LineStyle.stretchWidth = true;
}
return _LineStyle;
}
}
显示的结果如下:
接着:
materialEditor.serializedObject.Update();
var mShader = materialEditor.serializedObject.FindProperty("m_Shader");
调用了当前编辑器下序列化对象的Update方法,这个方法的解释,我在官网上只找到了一行的解释:
https://docs.unity3d.com/ScriptReference/SerializedObject.Update.html
Update serialized object's representation.
更新序列化对象的声明。
然后是用序列化对象找到属性"m_Shader“,注意这里的属性名必须是m_Shader,否则找不到对应的shader。这是unity内部的约定,不可更改。
private Stack<bool> toggledGroups = new Stack<bool>();
。。。。
toggledGroups.Clear();
栈的清空。
接着:
if (materialEditor.isVisible/* && !mShader.hasMultipleDifferentValues && mShader.objectReferenceValue != null*/)
{
这里后面两个hasMultipleDifferentValues 和objectReferenceValue 不知道干嘛,我暂时去掉了。只保留一个判断条件,就是当编辑器可见的时候才去更新。
EditorGUIUtility.labelWidth = TCP2_Utils.ScreenWidthRetina - 120f;
EditorGUIUtility.fieldWidth = 64f;
EditorGUI.BeginChangeCheck();
EditorGUI.indentLevel++;
设置了label的宽度;输入区域的宽度;开始检测是否有子空间变化;设置缩进方式;
foreach (var p in properties)
{
……
}
遍历材质球或者叫做shader的每个属性。
就是这里的属性:
Shader "Toony Colors Pro 2/Examples/Cat Demo/UnityChan/Style 1 Skin"
{
Properties
{
……在这里的属性
}
}
接着:
这两个我们先不看。
它的意思是,如果属性名称是以__BeginGroup和__EndGroup开头的,则放在一个文件夹下,可以折叠显示。
我们这里只看最后的else语句。
//Draw regular property
if (visible && (p.flags & (MaterialProperty.PropFlags.PerRendererData | MaterialProperty.PropFlags.HideInInspector)) == MaterialProperty.PropFlags.None)
mMaterialEditor.ShaderProperty(p, p.displayName);
这里的visible标志在foreach的开始:
var visible = (toggledGroups.Count == 0 || toggledGroups.Peek());
当然这里的toggledGroups.Count=0,所以visible为true;后面的是说明如果某个属性标志位为PerRendererData 或者HideInInspector都不会显示在Inspector面板上。
否则使用mMaterialEditor.ShaderProperty(p, p.displayName);的方法绘制属性。
然后负责更新刷新:
EditorGUI.indentLevel--;
if (EditorGUI.EndChangeCheck())
{
materialEditor.PropertiesChanged();
}
最后:
#if UNITY_5_5_OR_NEWER
TCP2_GUI.Separator();
materialEditor.RenderQueueField();
#endif
#if UNITY_5_6_OR_NEWER
materialEditor.EnableInstancingField();
#endif
这个对应的显示的是:
ok,至此我们的TCP2_MaterialInspector_SG编辑器类已经分析完毕,处理哪个折叠显示的没有分析,后面会补上。
下面分析下shader中的之前没有见过的东西:
首先是这个属性的头:
[TCP2HeaderHelp(BASE, Base Properties)]
显示的效果如下:
这个到底是啥?在TCP2_GUI中,可以找到:
public class TCP2HeaderHelpDecorator : MaterialPropertyDrawer
{
protected readonly string header;
protected readonly string help;
public TCP2HeaderHelpDecorator(string header)
{
this.header = header;
help = null;
}
public TCP2HeaderHelpDecorator(string header, string help)
{
this.header = header;
this.help = help;
}
public override void OnGUI(Rect position, MaterialProperty prop, string label, MaterialEditor editor)
{
TCP2_GUI.HeaderAndHelp(position, header, null, help);
}
定义了一个绘制类,继承自MaterialPropertyDrawer,我们上面知道了:
TCP2HeaderHelpDecorator 会把TCP2HeaderHelp作为标签的名字;
此类有两个成员:header和help。
在OnGUI中,进行绘制。
public static void HeaderAndHelp(Rect position, string header, string tooltip, string helpTopic)
{
if(!string.IsNullOrEmpty(helpTopic))
{
var btnRect = position;
btnRect.width = 16;
//Button
if(GUI.Button(btnRect, new GUIContent("", "Help about:\n" + helpTopic), HelpIcon))
OpenHelpFor(helpTopic); //打开帮助文档使用,暂时可以忽略
}
//Label
position.x += 16;
position.width -= 16;
GUI.Label(position, new GUIContent(header, tooltip), EditorStyles.boldLabel);
}
ok,这里知道了属性的绘制方式。
也就是说,shader有自定义的编辑器,属性也可以有自己的绘制方式。
再往下:
[TCP2Separator]
其绘制类为:
public class TCP2SeparatorDecorator : MaterialPropertyDrawer
{
public override void OnGUI(Rect position, MaterialProperty prop, string label, MaterialEditor editor)
{
position.y += 4;
position.height -= 4;
TCP2_GUI.Separator(position);
}
public override float GetPropertyHeight(MaterialProperty prop, string label, MaterialEditor editor)
{
return 12f;
}
}
绘制空格符的代码:
public static void Separator(Rect position)
{
var colorDark = EditorGUIUtility.isProSkin ? new Color(.1f, .1f, .1f) : new Color(.3f, .3f, .3f);
var colorBright = EditorGUIUtility.isProSkin ? new Color(.3f, .3f, .3f) : new Color(.9f, .9f, .9f);
var lineRect = position;
lineRect.height = 1;
GUILine(lineRect, colorDark, 1); //上面介绍了画线的方法了。
lineRect.y += 1;
GUILine(lineRect, colorBright, 1);
}
改下颜色试试:
public static void Separator(Rect position)
{
var colorDark = EditorGUIUtility.isProSkin ? new Color(.1f, .1f, .1f) : new Color(.3f, .3f, .3f);
var colorBright = EditorGUIUtility.isProSkin ? new Color(.3f, .3f, .3f) : new Color(.9f, .9f, .9f);
var lineRect = position;
lineRect.height = 1;
GUILine(lineRect, new Color(1,1,0,1), 1);
//lineRect.y += 1;
//GUILine(lineRect, colorBright, 1);
}
这就是分割线了,是不是很好玩了。
ok,至此我们已经分析了shader编辑器的编写,以及属性的绘制类重载。