同样的着色器,不同的贴图
用户界面
到目前为止,我们一直都为我们的材质使用Unity默认的材质监视器,它很耐用,但是Unity的标准着色器长得非常不一样。仿照着标准着色器,让我们一起来为我们自己的着色器创建一个自定义监视器吧。
我们默认的监视器和标准着色器监视器
着色器GUI
我们可以通过添加一个继承UnityEditor.ShaderGUI的类创建一个自定义监视器。因为它是一个编辑器类,所以应该把脚本文件放到Editor文件夹下。
usingUnityEngine;
usingUnityEditor;
publicclassMyLightingShaderGUI : ShaderGUI {
}
我们不需要继承MaterialEditor吗?
Unity 4.1支持通过继承MaterialEditor自定义材质监视器,你也可以依旧这样做,但是ShaderGUI在5.0版本中是作为备选被加入的,它的创建与Substance材质有一些关系。Unity为标准着色器使用ShaderGUI,所以我们也使用它。
要使用一个自定义GUI,你需要向着色器加入CustomEditor指令,紧跟着一个字符串,字符串包含要使用的GUI类的名字。
Shader"Custom/My First Lighting Shader"{
…
CustomEditor"MyLightingShaderGUI"
}
ShaderGUI类能放入命名空间吗?
可以。你需要在着色器中指定完全限定的类名。
CustomEditor"MyNamespace.MyShaderGUI"
为了替换默认监视器,我们需要重写ShaderGUI.OnGUI方法,该方法有两个参数,第一个参数是MaterialEditor的引用,这个对象管理当前被选材质的监视器;第二个参数是包含材质属性的数组。
publicclassMyLightingShaderGUI : ShaderGUI {
publicoverridevoidOnGUI (
MaterialEditor editor, MaterialProperty[] properties
) {
}
}
在这个方法内部,我们已经创建了我们自己的GUI。因为我们还什么都没做,所以监视器还是空的。
创建一个标签
标准着色器GUI被分成两部分,一部分针对主要贴图,另一部分针对次要贴图。我们会在我们的GUI中采用相同的布局。为了保持代码整洁,我们会为GUI不同的部分采用单独的方法。我们从主要部分以及其标签入手。
publicoverridevoidOnGUI ( MaterialEditor editor, MaterialProperty[] properties ) {
DoMain();
}
voidDoMain() {
GUILayout.Label("Main Maps");
}
主要贴图标签
GUILayout是如何工作的?
UnityEditor是用Unity的立即模式UI创建的,这是Unity老的UI系统,在当今基于画布的系统之前,它也用于游戏内UI。
立即模式UI的基础是GUI类,它包含创建UI小工具的方法。你需要利用矩形来精确定位每个元素。GUILayout类提供了相同的功能,但是它会使用简单布局系统自动定位小工具。
除此之外,EditorGUI类和EditorGUILayout类提供了针对编辑器UI的小工具和特征的访问。
标准着色器有一个粗体标签,所以我们也想要一个粗体标签,通过向标签添加GUI样式就可以实现,这样一来就是EditorStyles.boldLabel。
GUILayout.Label("Main Maps", EditorStyles.boldLabel);
粗体标签.
显示Albedo
为了显示我们材质的属性,我们需要在我们的方法中访问它们。我们会把OnGUI的参数传递到其他所有方法中,但是这会导致大量重复代码。我们不这么做,我们把它们放到域中。
MaterialEditor editor;
MaterialProperty[] properties;
publicoverridevoidOnGUI ( MaterialEditor editor, MaterialProperty[] properties ) {
this.editor = editor;
this.properties = properties;
DoMain();
}
每次OnGUI被调用时我们都需要拷贝引用吗?
当一个新的ShaderGUI实例被创建时,MaterialEditor会做出决定。目前来讲,这会在一个材质被选了的时候发生,就像你可能预期的那样。但是它也可能在撤销会重做动作执行的时候发生。这意味着你不能继续依赖ShaderGUI实例了。每次,它都会是一个新的对象实例,你可以把OnGUI想成它好像是一个静态方法,尽管它并不是。
Albedo贴图在标准着色器中第一次展现。这是主要纹理,它的属性在属性数组的某一位置设置,它的数组下标取决于我们着色器中定义属性的顺序。但是通过名称查找的鲁棒性更强一些。ShaderGUI包括FindProperty方法,它就是干这件事的,通过给定名称和属性数组找到对应结果。
voidDoMain () {
GUILayout.Label("Main Maps", EditorStyles.boldLabel);
MaterialProperty mainTex = FindProperty("_MainTex", properties);
}
除了纹理属性之外,我们也需要定义标签的内容,这可以通过GUIContent实现,它是一个简单的容器类。
MaterialProperty mainTex = FindProperty("_MainTex", properties);
GUIContent albedoLabel =newGUIContent("Albedo");
但是我们已经在我们的着色器中为主要纹理Albedo命名了,我们只能使用已定义好的名称,我们可以通过属性访问到。
GUIContent albedoLabel =newGUIContent(mainTex.displayName);
为了创建那些小的纹理小工具,我们必须依赖我们已经引用的编辑器,它具有许多绘制这样小工具的方法。
MaterialProperty mainTex = FindProperty("_MainTex", properties);
GUIContent albedoLabel =newGUIContent(mainTex.displayName);
editor.TexturePropertySingleLine(albedoLabel, mainTex);
Albedo贴图
这就是看起来像标准着色器的开始!但是当你鼠标经过属性标签时,那个监视器还有提示信息,在Albedo贴图的情况中,上面写着Albedo (RGB)和Transparency (A)。
我们也可以加一个提示信息,很简单,只要向标签内容中添加就可以了。因为我们还不支持透明度,我们就只使用Albedo(RGB)。
GUIContent albedoLabel =
newGUIContent(mainTex.displayName,"Albedo (RGB)");
带提示信息的Albedo
TexturePropertySingleLine方法有可以与多个(最多三个)属性作用的变体,第一个应该是纹理,但是其他可能就是别的什么了,它们都会被放在一行中,我们可以用这来显示纹理旁边的取色工具。
MaterialProperty tint = FindProperty("_Tint", properties);
editor.TexturePropertySingleLine(albedoLabel, mainTex, tint);
Albedo贴图和取色
让我们直接跳到主要部分的底部,那是显示主要纹理贴砖和偏移量的位置,这可以通过MaterialEditor.TextureScaleOffsetProperty方法实现。
editor.TexturePropertySingleLine(albedoLabel, mainTex, tint);
editor.TextureScaleOffsetProperty(mainTex);
便利方法
不用现有的FindProperty方法,让我们创建一个只需要一个名称参数的方法,充分利用我们属性域的优势,这会让我们的代码更易读。
MaterialProperty FindProperty (stringname) {
returnFindProperty(name, properties);
}
在DoMain中换成用这个方法,我们也可以直接把色彩属性传递给TexturePropertySingleLine方法,我们不会在其它地方用到它。
voidDoMain () {
GUILayout.Label("Main Maps", EditorStyles.boldLabel);
MaterialProperty mainTex = FindProperty("_MainTex");
// MaterialProperty tint = FindProperty("_Tint", properties);
GUIContent albedoLabel =
newGUIContent(mainTex.displayName,"Albedo (RGB)");
editor.TexturePropertySingleLine(
albedoLabel, mainTex, FindProperty("_Tint")
);
editor.TextureScaleOffsetProperty(mainTex);
}
让我们再建立一个方法来配置标签的内容。我们只需要使用一个单例静态GUIContent就可以,我们把它的文本和提示信息修改了就好。因为我们可能不总是需要提示信息,让我们把它变为可选择的,同时带有一个默认的参数值。
staticGUIContent staticLabel =newGUIContent();
staticGUIContent MakeLabel (stringtext,stringtooltip =null) {
staticLabel.text = text;
staticLabel.tooltip = tooltip;
returnstaticLabel;
}
如果我不非要总是从属性中抽取出显示名称的话,就会更加方便,所以再建立一个MakeLabel变量来做这件事吧。
staticGUIContent MakeLabel (
MaterialProperty property,stringtooltip =null
) {
staticLabel.text = property.displayName;
staticLabel.tooltip = tooltip;
returnstaticLabel;
}
现在DoMain变得更小了,我们日后的方法也会遵循这一重构流程。
voidDoMain () {
GUILayout.Label("Main Maps", EditorStyles.boldLabel);
MaterialProperty mainTex = FindProperty("_MainTex");
// GUIContent albedoLabel =
// new GUIContent(mainTex.displayName, "Albedo (RGB)");
editor.TexturePropertySingleLine(
MakeLabel(mainTex,"Albedo (RGB)"), mainTex, FindProperty("_Tint")
);
editor.TextureScaleOffsetProperty(mainTex);
}
显示法线
下一个需要展示的纹理是法线贴图。不要把所有代码都放在DoMain中,把代码分派到一个独立的DoNormals方法中,在Albedo行之后、铺砖和偏移量之前调用。
DoNormals();
editor.TextureScaleOffsetProperty(mainTex);
新的DoNormals方法会检索贴图属性,之后展现出来,标准着色器不会提供任何额外的提示信息,所以我们也不会。
voidDoNormals () {
MaterialProperty map = FindProperty("_NormalMap");
editor.TexturePropertySingleLine(MakeLabel(map), map);
}
当然也有凹凸程度,所以把它加到代码中。
editor.TexturePropertySingleLine(
MakeLabel(map), map, FindProperty("_BumpScale")
);
法线贴图和凹凸程度
标准着色器只会在法线贴图赋值给材质的时候显示凹凸程度,我们也这样做,检查属性是否引用了一个纹理,如果引用了,就显示凹凸程度,否则就只是给TexturePropertySingleLine赋空值。
editor.TexturePropertySingleLine(
MakeLabel(map), map,
map.textureValue ? FindProperty("_BumpScale") :null
);
隐藏的凹凸程度
显示金属质感与光滑度
金属和光滑度属性是简单的浮点范围。至少目前我们可以通过通用的MaterialEditor.ShaderProperty方法显示它们。与纹理方法不同,该方法的第一个参数是属性,第二个参数是标签内容。
voidDoMain () {
…
editor.TexturePropertySingleLine(
MakeLabel(mainTex,"Albedo (RGB)"), mainTex, FindProperty("_Tint")
);
DoMetallic();
DoSmoothness();
DoNormals();
editor.TextureScaleOffsetProperty(mainTex);
}
…
voidDoMetallic () {
MaterialProperty slider = FindProperty("_Metallic");
editor.ShaderProperty(slider, MakeLabel(slider));
}
voidDoSmoothness () {
MaterialProperty slider = FindProperty("_Smoothness");
editor.ShaderProperty(slider, MakeLabel(slider));
}
金属质感与光滑度
我们可以让这些属性与其他标签对齐,通过提升编辑器的缩进级别。在这种情况下,两步。缩进级别可以通过静态EditorGUI.indentLevel属性调整,确保之后把它设置回原来的值。
voidDoMetallic () {
MaterialProperty slider = FindProperty("_Metallic");
EditorGUI.indentLevel += 2;
editor.ShaderProperty(slider, MakeLabel(slider));
EditorGUI.indentLevel -= 2;
}
voidDoSmoothness () {
MaterialProperty slider = FindProperty("_Smoothness");
EditorGUI.indentLevel += 2;
editor.ShaderProperty(slider, MakeLabel(slider));
EditorGUI.indentLevel -= 2;
}
锯齿属性
显示次要贴图
次要贴图与主要贴图看起来很像,所以创建一个DoSecondary方法,它可以管理粗体标签,细节纹理,以及它的贴砖和偏移量。
publicoverridevoidOnGUI (
MaterialEditor editor, MaterialProperty[] properties
) {
this.editor = editor;
this.properties = properties;
DoMain();
DoSecondary();
}
…
voidDoSecondary () {
GUILayout.Label("Secondary Maps", EditorStyles.boldLabel);
MaterialProperty detailTex = FindProperty("_DetailTex");
editor.TexturePropertySingleLine(
MakeLabel(detailTex,"Albedo (RGB) multiplied by 2"), detailTex
);
editor.TextureScaleOffsetProperty(detailTex);
}
调整我们着色器中细节纹理的显示名称,让它与标准着色器一致。
1
_DetailTex ("Detail Albedo", 2D) ="gray"{}
次要贴图
细节法线贴图与主要法线贴图的效果一样,有趣的是,标准着色器GUI没有隐藏细节凹凸程度,但是我们要求一致性,所以当没有细节法线贴图的时候,我们把细节凹凸程度也隐藏。
voidDoSecondary () {
…
DoSecondaryNormals();
editor.TextureScaleOffsetProperty(detailTex);
}
voidDoSecondaryNormals () {
MaterialProperty map = FindProperty("_DetailNormalMap");
editor.TexturePropertySingleLine(
MakeLabel(map), map,
map.textureValue ? FindProperty("_DetailBumpScale") :null
);
}
完整监视器
混合金属与非金属
因为我们的着色器使用uniform值来判断某个物体有多金属化,对于一个材质表面来说,它不能改变,这会阻碍我们创建复杂材质,这些材质实际上代表了不同材质的混合。例如,这里有计算机电路的艺术印象的Albedo和法线贴图。
电路的Albedo和法线贴图
绿色部分形成了电路板的基础,蓝色部分代表光,这些是非金属的。金黄色部分代表导电回路,这里应该是金属的。在顶部有一些棕色污点,是为了使变化丰富。
用这些贴图创建一个新的材质,同时使用我们的光照着色器,让效果相对光滑些。同时,因为材质不是明亮的,它与Unity默认的周边环境相互作用,所以如果你的场景的Ambient Intensity值比0小,把它设回1。
电路材质
使用Metallic滑块,我们可以让整个表面变得非金属、金属或介于两者之间,这对于电路并不足够。
uniform非金属和金属
金属度贴图
标准着色器支持金属度贴图,这些贴图每纹素都定义了金属值,而不是一次性为整个材质都定义。这里有一个灰度贴图,它把电路作为金属标记,余下的是非金属。有污渍的金属暗一些,因为表层上有半透明的脏东西。
金属度贴图
为着色器中这样的贴图添加一个属性。
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) ="white"{}
[NoScaleOffset] _NormalMap ("Normals", 2D) ="bump"{}
_BumpScale ("Bump Scale", Float) = 1
[NoScaleOffset] _MetallicMap ("Metallic", 2D) ="white"{}
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
_DetailTex ("Detail Albedo", 2D) ="gray"{}
[NoScaleOffset] _DetailNormalMap ("Detail Normals", 2D) ="bump"{}
_DetailBumpScale ("Detail Bump Scale", Float) = 1
}
我们还需要NoScaleOffset属性吗?
那些属性是默认着色器GUI的提示,那么,我们就不再需要它们了,我在教程中保留它们,只是为了提示那些需要检验着色器代码的人。
也要在我们的include文件中加入相应的变量。
1
2sampler2D _MetallicMap;
float_Metallic;
让我们建立一个函数来检索一个片段的金属值,利用Interpolators作为参数,它简单地对金属度贴图进行了采样,并且用它和uniform金属值相乘。Unity用贴图的R通道,所以我们也用这个通道。
structInterpolators {
…
};
floatGetMetallic (Interpolators i) {
returntex2D(_MetallicMap, i.uv.xy).r * _Metallic;
}
现在我们可以检索MyFragmentProgram中的金属值。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
…
albedo = DiffuseAndSpecularFromMetallic(
albedo, GetMetallic(i), specularTint, oneMinusReflectivity
);
…
}
注意MyFragmentProgram代码不关心金属值是如何得到的,如果你想要以不同的方式定义金属值,你只需要修改GetMetallic。
自定义GUI
如果我们仍然使用默认着色器GUI,金属度贴图就会出现在监视器中了,但是现在我们需要通过调整DoMetallic把它明确地加入到MyLightingShaderGUI中,就像标准着色器一样,我们把贴图和滑块在一行中显示。
voidDoMetallic () {
MaterialProperty map = FindProperty("_MetallicMap");
editor.TexturePropertySingleLine(
MakeLabel(map,"Metallic (R)"), map,
FindProperty("_Metallic")
);
}
使用一个金属度贴图
贴图或者滑块
当使用金属度贴图时,标准着色器的GUI会隐藏滑块,我们也可以这么做,它与凹凸程度的效果一样,当没有纹理的时候就会显示值。
editor.TexturePropertySingleLine(
MakeLabel(map,"Metallic (R)"), map,
map.textureValue ?null: FindProperty("_Metallic")
);
隐藏的滑块
自定义着色器关键字
金属滑块隐藏了,因为标准着色器用一个贴图或者一个uniform值,它们不是乘在一起的。当提供了金属度贴图时,uniform值就会被忽略。为了使用相同的方式,我们需要区分带有和没有金属度贴图的材质,这可以通过个两个着色器变体实现,一个有贴图,一个没有贴图。
由于着色器中的#pragma multi_compile指令,我们的着色器已经有多个变体生成,它们是基于Unity提供的关键字。通过定义我们自己的着色器关键字,我们可以创建我们需要的变体。
你可以任意命名自定义关键字,但是传统是使用一个以下划线开头的大写单词命名,在这个例子中,我们使用_METALLIC_MAP。
自定义关键字在哪里定义?
Unity检测项目中的所有自定义关键字,基于multi-compile语句,这些语句把关键字加入材质中。在内部,它们被转换,合并入位掩码,而且每个项目都可以得到不同的关键字标识符。
在Unity 5.4中,位掩码有128位。因此,每个项目可以存在最多128个关键字。其中包括Unity的关键字,加上任何正在使用的自定义关键字。这个限制通常会低一些,这会让具有许多关键字的着色器成为潜在的危险。Unity 5.5会把限制值增加到256。
要向材质中添加自定义关键字,我们必须在我们的GUI中直接访问材质。我们可以通过MaterialEditor.target属性得到当前选择的材质,这实际上是来自Editor基类的一个继承属性,它是通用的Object类型,所以我们必须把它强制转换成Material。
Material target;
MaterialEditor editor;
MaterialProperty[] properties;
publicoverridevoidOnGUI (
MaterialEditor editor, MaterialProperty[] properties
) {
this.target = editor.targetasMaterial;
this.editor = editor;
this.properties = properties;
DoMain();
DoSecondary();
}
Material.EnableKeyword方法可以向着色器中添加一个关键字,其中关键字的名称作为参数。对于移除一个关键字,有Material.DisableKeyword方法。我们一起来创建一个便利的方法,它可以通过一个布尔型参数启用或者禁用一个关键字。
voidSetKeyword (stringkeyword,boolstate) {
if(state) {
target.EnableKeyword(keyword);
}
else{
target.DisableKeyword(keyword);
}
}
调试关键字
你可以使用调试监视器来证实我们的关键字已经加入材质或者从材质中移除,你可以通过选项卡右上的下拉菜单把监视器转到调试模式。自定义关键字在Shader Keywords文本域中以列表形式显示。
调试监视器
你在这里找到的任何预料不到的着色器关键字都已经被定义,因为之前的着色器已经赋值给材质。例如,你选择一个新材质,标准着色器GUI就会添加一个_EMISSION关键字, 它们对于我们的着色器是毫无用处的,所以把它们从列表中移走。
着色器特征
为了生成着色器变体,我们需要向着色器添加另一个multi-compile指令,对于基础通道和附加通道都要这么做,阴影通道不需要。
#pragma target 3.0
#pragma multi_compile _ _METALLIC_MAP
当显示着色器变体时,你会发现我们的自定义关键字已经包含进来了,基础通道现在有总共八个变体。
// Total snippets: 3
// -----------------------------------------
// Snippet #0 platforms ffffffff:
SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP
8 keyword variants usedinscene:
VERTEXLIGHT_ON
SHADOWS_SCREEN
SHADOWS_SCREEN VERTEXLIGHT_ON
_METALLIC_MAP
VERTEXLIGHT_ON _METALLIC_MAP
SHADOWS_SCREEN _METALLIC_MAP
SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP
当使用multi-compile指令时,Unity会为所有可能的组合生成着色器变体。当使用很多关键字时,编译所有的排列会花费很多时间,所有这些变体都包含在构建项目中,这可能是不必要的。
另一个可行方法是定义一个着色器 ,而不是multi-compile指令,区别在于着色器特征的排列只在需要时编译,如果没有材质使用某个关键字,那么关于此关键字的着色器变体将不会被编译。Unity也会检查哪些关键字用在构建项目阶段,只会包含必需的着色器变体。
那么,让我们为我们的自定义关键字使用#pragma shader_feature。
#pragma shader_feature _ _METALLIC_MAP
你什么时候可以使用着色器特征?
当材质在设计阶段配置时——只在编辑器中——之后你大可放心使用着色器特征。但是如果你在运行时调整了材质的关键字,那么你必须确保所有的变体都包含进来了,最简单的方法就是紧跟相关关键字multi-compile指令。或者,你可以使用一个着色器变体集资源。
如果着色器特征是一个关键词的触发器,你可以忽略单下划线。
#pragma shader_feature _METALLIC_MAP
在做了这个改变之后,所有的变体仍旧列了一张表,尽管Unity排列的顺序可能不一样。
// Total snippets: 3
// -----------------------------------------
// Snippet #0 platforms ffffffff:
_METALLIC_MAP
SHADOWS_SCREEN VERTEXLIGHT_ON
8 keyword variants usedinscene:
_METALLIC_MAP
VERTEXLIGHT_ON
VERTEXLIGHT_ON _METALLIC_MAP
SHADOWS_SCREEN
SHADOWS_SCREEN _METALLIC_MAP
SHADOWS_SCREEN VERTEXLIGHT_ON
SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP
最后,在我们的include文件中调整GetMetallic函数,当定义了_METALLIC_MAP之后,取样贴图,否则就返回uniform值。
floatGetMetallic (Interpolators i) {
#if defined(_METALLIC_MAP)
returntex2D(_MetallicMap, i.uv.xy).r;
#else
return_Metallic;
#endif
}
所以只能用_MetallicMap或者_Metallic之一,而不是全用?
确实是这样,所以材质总会有至少一个无用的属性,为了灵活性,得有点开销。
只在需要的时候设置关键字
此刻,每当OnGUI被调用的时候我们就设置材质的关键词,这种调用很频繁。逻辑上,我们只需要在贴图属性已经被编辑的时候做这件事,我们可以检查通过使用EditorGUI.BeginChangeCheck和EditorGUI.EndChangeCheck方法是否有改变产生。第一个方法定义了我们想要开始追踪变化的点,第二个方法标记了结尾,并且返回是否产生了变化。
通过把这些方法放在TexturePropertySingleLine之前和之后,我们可以很容易地检测金属行是否被编辑了。如果是这样,我们设置关键字。
voidDoMetallic () {
MaterialProperty map = FindProperty("_MetallicMap");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(
MakeLabel(map,"Metallic (R)"), map,
map.textureValue ?null: FindProperty("_Metallic")
);
if(EditorGUI.EndChangeCheck()) {
SetKeyword("_METALLIC_MAP", map.textureValue);
}
}
当_Metallic改变时是否也会触发该方法?
是的,当贴图改变时,当uniform值被编辑时,代码都会设置关键字。这比要求的频繁,但是也比总设置强。
这会支持撤销和重做吗?
是的。我们正在使用显示属性的MaterialEditor方法来负责记录原对象的状态。
光滑度贴图
就像金属度贴图一样,光滑度也可以通过一个贴图定义。这里的电路有一个灰度光滑度纹理。金属部分是最光滑的,之后是灯泡,剩余部分非常粗糙。污渍比电路板光滑,所以那里的纹理更浅。
光滑度贴图
Unity的标准着色器希望把光滑度存储在alpha通道中。实际上,希望金属度和光滑度贴图可以在相同的纹理中结合起来,因为DXT5把RGB和A通道分别压缩,把贴图合并到一个DXT5纹理中,可以产生与使用两个DXT1纹理相同的质量。这不会要求更少的内存,但是可以让我们从一次纹理采样中就检索到金属度和光滑度,而不是两次。
有一个纹理合并了贴图。尽管金属度只需要R通道,我依旧会用金属度值填充RGB通道,光滑度使用alpha通道。
金属度和光滑度贴图
决定光滑度
当存在一个金属度贴图时,我们可以从中得到光滑度,否则的话,我们就使用uniform_Smoothness属性,向我们的include文件中加入一个GetSmoothness函数来管理它,这个函数与GetMetallic几乎一模一样。
floatGetSmoothness (Interpolators i) {
#if defined(_METALLIC_MAP)
returntex2D(_MetallicMap, i.uv.xy).a;
#else
return_Smoothness;
#endif
}
我们不还是要取样纹理两次吗?
记住着色器编译器摆脱了重复代码,我们正在不同的函数中采样相同的纹理,但是编译好的代码只会取样纹理一次,我们不需要明确地对这些内容进行缓存。
实际上,标准着色器有两个不同的光滑度属性,一个是单独的uniform值,就像我们的一样,另一个是调整光滑度贴图的标量。让我们简单点做,为了两个目标用_Smoothness属性,这意味着你需要把它设为1来获取未修改的光滑度贴图值。
returntex2D(_MetallicMap, i.uv.xy).a * _Smoothness;
用这个新函数在MyFragmentProgram中得到光滑度。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
…
returnUNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, GetSmoothness(i),
i.normal, viewDir,
CreateLight(i), CreateIndirectLight(i, viewDir)
);
}
但是那不是我们唯一使用光滑度的地方,CreateIndirectLight也会使用光滑度。我们可以向这个函数中加入一个光滑度参数,但是我们也可以再调用一次GetSmoothness,着色器编译器会检测重复代码,并且优化。
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
…
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - GetSmoothness(i);
…
}
贴图的光滑度,强度最高
那些沿着金属条边缘的方形物件是什么?
那些物件是由于法线贴图的DXT5nm纹理压缩产生的。特别地,如果太瘦的窄脊与紫外线轴不是对齐的,就不能正确估测这些脊。电路中陡峭的对角线边缘是这种压缩最坏的情况,这种局限性在表面上会变得更清晰可见,这些表面是金属的而且非常光滑,否则的话,就不那么明显。
使用未压缩的法线贴图
合并光滑度和Albedo
当你两个都需要时,把金属度和光滑度贴图合并入单一的纹理中很好。金属部分几乎总是比其它位的光滑度高,所以当你需要金属度贴图时,实际上你也总是需要一个光滑度贴图。但是你也有可能需要一个光滑度贴图,而不需要混合金属和非金属,在这种情况下,金属度贴图是没有用的。
对于那些不需要金属度贴图的不透明材质来说,可以在Albedo贴图的阿尔法通道中储存光滑度,这种用法很普遍,标准着色器支持在金属度贴图或者Albedo贴图中打包光滑度,让我们也这么做。
在关键词间转换
就像标准着色器一样,我们需要加一个选项来选择我们GUI的光滑度源。尽管标准着色器只支持两个贴图之间的选择,但是我们可以不用管它,直接把uniform光滑性也作为第三选项包含进来。为了体现这些选项,在MyLightingShaderGUI中定义一个枚举类型。
1
2
3enumSmoothnessSource {
Uniform, Albedo, Metallic
}
当Albedo贴图也作为光滑度源使用时,我们会向材质中添加_SMOOTHNESS_ALBEDO关键字。当使用金属度源时,我们会加入_SMOOTHNESS_ALBEDO来代替。同时uniform选项不对应任何关键字。
标准着色器也使用了一个浮点属性来追踪某个材质用了什么选项,但是我们可以只针对关键字实现这一点。GUI可以通过检查启用了哪些关键字来确定当前选择,这可以通过Material.IsKeywordEnabled方法实现,我们将会创建一个简便的封装。
boolIsKeywordEnabled (stringkeyword) {
returntarget.IsKeywordEnabled(keyword);
}
现在DoSmoothness可以找出已选材质当前的光滑度源。
voidDoSmoothness () {
SmoothnessSource source = SmoothnessSource.Uniform;
if(IsKeywordEnabled("_SMOOTHNESS_ALBEDO")) {
source = SmoothnessSource.Albedo;
}
elseif(IsKeywordEnabled("_SMOOTHNESS_METALLIC")) {
source = SmoothnessSource.Metallic;
}
…
}
为了显示选项,我们可以用EditorGUILayout.EnumPopup方法,另外增加缩进级别,来与标准着色器的布局相匹配。
EditorGUI.indentLevel += 2;
editor.ShaderProperty(slider, MakeLabel(slider));
EditorGUI.indentLevel += 1;
EditorGUILayout.EnumPopup(MakeLabel("Source"), source);
EditorGUI.indentLevel -= 3;
光滑度源弹出
EnumPopup是一个基本编辑器小工具,它可以为任何枚举创建弹出列表,它返回选择的值。如果用户没有选择一个新选项,返回值与原始选择相同,否则的话,值就不同。所以,要想知道选择了哪个选项,我们需要把值赋回给源变量。由于该方法是通用枚举类型的,我们需要把它强制转换成SmoothnessSource。
source = (SmoothnessSource)EditorGUILayout.EnumPopup(
MakeLabel("Source"), source
);
如果有改变产生,我们用源变量来控制应该设置哪个关键字,假如有的话。
EditorGUI.BeginChangeCheck();
source = (SmoothnessSource)EditorGUILayout.EnumPopup(
MakeLabel("Source"), source
);
if(EditorGUI.EndChangeCheck()) { SetKeyword("_SMOOTHNESS_ALBEDO", source == SmoothnessSource.Albedo);
SetKeyword("_SMOOTHNESS_METALLIC", source == SmoothnessSource.Metallic);
}
来自金属度贴图的光滑度
支持撤销
我们现在可以改变光滑度源,但是它还并不支持撤销和重做动作,因为我们正在使用基本小工具,我们必须手动表示我们进行了一个支持撤销的动作。这可以通过MaterialEditor.RegisterPropertyChangeUndo方法实现,它有一个参数是描述标签。为这个方法也建立一个封装。
voidRecordAction (stringlabel) {
editor.RegisterPropertyChangeUndo(label);
}
RecordAction必须在我们要改变的部分之前调用,它会为原状态创建一个快照,这样一来撤销动作就可以恢复到原状态。
if(EditorGUI.EndChangeCheck()) {
RecordAction("Smoothness Source");
SetKeyword("_SMOOTHNESS_ALBEDO", source == SmoothnessSource.Albedo);
SetKeyword(
"_SMOOTHNESS_METALLIC", source == SmoothnessSource.Metallic
);
}
光滑度变量
为了全部支持三个选项,加一个着色器特征,特征在没有关键字、_SMOOTHNESS_ALBEDO,和_SMOOTHNESS_METALLIC中选择。如之前,基本和附加通道都必须支持它。
#pragma shader_feature _METALLIC_MAP
#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
在GetSmoothness中,首先从光滑度为1开始,之后检查是否选择了Albedo源,如果选择了,Albedo贴图替换1;否则的话,检查是否选择了金属度源,如果选择了,用金属度源取而代之。当然了,只有当材质确实使用了金属度贴图的时候这才合理,所以也要检查材质是否使用了金属度贴图。
在那之后,返回我们得到的任何光滑度值,乘上_Smoothness属性的值,如果我们最终的变体没有使用贴图,编译器会乘上1来进行优化。
floatGetSmoothness (Interpolators i) {
floatsmoothness = 1;
#if defined(_SMOOTHNESS_ALBEDO)
smoothness = tex2D(_MainTex, i.uv.xy).a;
#elif defined(_SMOOTHNESS_METALLIC) && defined(_METALLIC_MAP)
smoothness = tex2D(_MetallicMap, i.uv.xy).a;
#endif
returnsmoothness * _Smoothness;
}
熔岩材质
这里是冷却熔岩印象的albedo及法线贴图。材质是非金属的,但是光滑度变化不定,所以光滑度值存在Albedo贴图的阿尔法通道中。
带有光滑度和法线的Albedo
用这些贴图创建一个材质,光滑度的Albedo源选项。
熔岩材质
当使用Albedo源时,结果是灰色硬块的光滑度会比红沟高得多。
使用Albedo阿尔法,Uniform和mapped
发射表面
到目前为止,我们只处理过了那些反射灯光的材质,通过漫反射或是镜面反射。我们需要一个光源来看到这些表面,但是也有一些表面自己会发射光亮。比如说:当一些东西变得足够热了,它就会开始发光,而你不需要一个不同的光源来看到它。标准着色器通过发射贴图和颜色支持这一点,我们也会支持。
Mapped和Uniform
向我们的着色器中添加发射贴图和颜色的属性。两者都应该默认是黑色的,这意味着不发射任何光。因为我们只关心RGB通道,我们可以省略默认颜色的第四组件。
[NoScaleOffset] _EmissionMap ("Emission", 2D) ="black"{}
_Emission ("Emission", Color) = (0, 0, 0)
许多材质没有发射贴图,所以,我们用着色器特征来创建需要和不需要发射贴图的变体,因为我们只需要加发射光一次,只在基本通道中包含特征。
#pragma shader_feature _METALLIC_MAP
#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
#pragma shader_feature _EMISSION_MAP
向include文件中加入所需的采样器和浮点变量。
sampler2D _EmissionMap;
float3 _Emission;
创建一个GetEmission函数来检索发射的颜色,假如有的话。当存在贴图时,取样颜色并且乘上uniform颜色;否则的话,就只返回uniform颜色。但是只需在基本通道中做这件麻烦事,在其它所有情况下,发射都是0,编译器会自动优化。
float3 GetEmission (Interpolators i) {
#if defined(FORWARD_BASE_PASS)
#if defined(_EMISSION_MAP)
returntex2D(_EmissionMap, i.uv.xy) * _Emission;
#else
return_Emission;
#endif
#else
return0;
#endif
}
由于发射自己来源于对象,它是独立于反射光的,所以就把它加到final颜色中吧。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
…
float4 color = UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, GetSmoothness(i),
i.normal, viewDir,
CreateLight(i), CreateIndirectLight(i, viewDir)
);
color.rgb += GetEmission(i);
returncolor;
}
向GUI添加发射
在MyLightingShaderGUI内部添加一个DoEmission方法,做这件事最快的方法是拷贝DoMetallic,再做一些改变。
voidDoEmission () {
MaterialProperty map = FindProperty("_EmissionMap");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(
MakeLabel(map,"Emission (RGB)"), map, FindProperty("_Emission")
);
if(EditorGUI.EndChangeCheck()) {
SetKeyword("_EMISSION_MAP", map.textureValue);
}
}
把它包含进入主要贴图部分。
voidDoMain () {
…
DoNormals();
DoEmission();
editor.TextureScaleOffsetProperty(mainTex);
}
带有发射贴图和颜色的监视器
HDR发射
标准着色器不使用发射的常规颜色,不同的是,它支持高动态区域颜色。这意味着颜色的RGB组件可以高于1,这样一来,你可以表现出非常明亮的颜色。
我们能看到亮度大于1的颜色吗?
在现实生活中,轰炸你的光子的数量没有限制。太阳也非常明亮,也很炫目。但是,计算机显示是有限的,你不能比1更高,有多亮取决于显示的亮度。
要在一面有意义的墙上使用HDR颜色,你必须实现色调映射。这意味着你将颜色从一个范围转换到一个范围。我们会在未来的教程中深入研究色调映射。HDR颜色也经常用于创建闪亮效果。
因为颜色属性是浮点向量,我们的值不止限于0到1。但是,标准颜色小工具的值本身是0到1。幸运的是,MaterialEditor包括TexturePropertyWithHDRColor方法,这就是特别为纹理加上一个HDR颜色属性准备的,它需要两个额外参数,第一个参数是HDR范围的选项;第二个参数是是否应该显示阿尔法通道,这并不是我们想要的。
voidDoEmission () {
…
editor.TexturePropertyWithHDRColor(
MakeLabel("Emission (RGB)"), map, FindProperty("_Emission"),
emissionConfig,false
);
…
}
HDR颜色小工具通过ColorPickerHDRConfig对象配置,该对象包括允许的亮度和曝光范围。标准着色器的亮度是从0到99,曝光度是从近于0到3.我们就简单地使用相同的范围。
staticColorPickerHDRConfig emissionConfig =
newColorPickerHDRConfig(0f, 99f, 1f / 99f, 3f);
带有HDR发射的监视器
在颜色选择器之后额外的值对应颜色的亮度,这应该是RGB通道最大的一个值,把发射颜色从转换到黑或白最快的方法就是把这个值设为0或1。
发射光的熔岩
这里有一个熔岩材质的发射贴图,它使沟壑中的熔岩发光发热,你可以通过调整uniform颜色来改变发射的亮度和色彩。
熔岩的发射贴图
我给发射贴图赋值了,但是它没显示?
在这种情况下,uniform发射颜色仍然是黑色,要想完全看到贴图,把颜色设置成白色。
当纹理已经被赋值但是颜色还是黑色的时候,标准着色器自动把发射颜色设成白色。你也可以把这个功能加上。但是,这种行为可能对于一些人来说不是真的,这会带来许多用户满意度的降低。
发光的容颜,点亮的和未点亮的
自发光的电路
这里是电路光照的放射贴图。
电路的放射贴图
光线有变化的亮度,污渍也会影响光线。
带有运行光照的电路,点亮的和未点亮的
发射的光会照亮其他物体吗?
发射只是材质的一部分,它不会影响场景的剩余部分。但是,Unity的全局照明系统可以检测到发射光,并且把它加到非直接照明数据中。我们会在之后的教程中深入了解全局照明。