Toony Colors Pro 2项目分析——shader编辑器

项目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编辑器的编写,以及属性的绘制类重载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值