本文重点:
1、创建自定义的着色器GUI 2、混合金属和非金属 3、使用非统一的平滑度 5、支持自发光表面
(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起,链接在文章底部)
这是关于渲染的系列教程的第九部分。上次,我们增加了对环境贴图的支持。在这一部分中,我们将结合多个纹理来创建复杂的材质。但是在开始之前,我们需要为着色器使用更好的GUI。
本教程使用Unity5.4.1f1制作。
(相同的材质,不同的贴图)
1 用户接口
到目前为止,我们一直在使用Unity的默认材质检查器作为材质。它是可维护的,但是Unity自己的标准着色器具有完全不同的外观。我们也为自己的着色器创建一个自定义检查器,以模仿标准着色器。
(我们默认的检查器 VS 标准着色器的检查器)
1.1 ShaderGUI
通过添加扩展UnityEditor.ShaderGUI的类来创建自定义检查器。由于它是编辑器类,因此需要将其脚本文件放置在Editor文件夹中。
我们不需要从MaterialEditor继承吗?
Unity 4.1通过扩展MaterialEditor添加了对自定义材质检查器的支持。你仍然可以执行此操作,但是ShaderGUI是在5.0中添加的。它的创建与材质有关。Unity使用ShaderGUI作为标准着色器,因此我们也使用它。
在后台,Unity将默认材质编辑器用于具有自定义ShaderGUI关联的着色器。该编辑器实例化GUI并调用其方法。
要使用自定义GUI,必须将CustomEditor指令添加到着色器,后面跟着包含要使用的GUI类名称的字符串。
ShaderGUI类可以放在命名空间中吗?
是的。但必须在着色器中指定完全限定的类名称。
要替换默认的检查器,我们必须重写ShaderGUI.OnGUI方法。此方法有两个参数。首先,对MaterialEditor的引用。该对象管理当前选定材质的检查器。其次,包含该材质属性的数组。
在此方法内,我们可以创建自己的GUI。由于我们尚未这样做,因此检查器是空的。
1.2 创建一个Label
标准着色器GUI分为两部分,一个部分用于主贴图,另一部分用于辅助贴图。我们将在GUI中使用相同的布局。为了保持代码的整洁,对GUI的不同部分使用单独的方法。从主要部分及其标签开始。
(主贴图标签)
GUILayout如何工作?
Unity编辑器是使用Unity的即时模式UI创建的。这是Unity的旧UI系统,在当前基于Canvas的系统之前,它还用于游戏中的UI。
即时模式UI的基础是GUI类。它包含创建UI小挂件的方法。你需要使用矩形来明确定位每个元素。GUILayout类提供相同的功能,同时使用简单的布局系统自动定位小挂件。
除此之外,EditorGUI和EditorGUILayout类还提供对编辑器UI的小挂件和功能的访问。
标准着色器具有一个粗体标签,因此我们也需要一个粗体标签。这可以通过在标签上添加GUI样式(在本例中为EditorStyles.boldLabel)来完成。
(粗体)
1.3 展示反照率
为了显示我们材质的特性,必须能在方法中访问它们。将OnGUI的参数传递给所有其他方法,但这会导致很多重复的代码。那么我们将它们放在字段中。
每次调用OnGUI时是否都需要复制引用?
MaterialEditor决定何时创建新的ShaderGUI实例。正如你所料,当选择材质时会发生这种情况。
但是,在执行撤消或重做操作时也会发生这种情况。这意味着你不能依赖ShaderGUI实例,因为每次都可能是一个新的对象实例。你可以将OnGUI视为静态方法,虽然它不是。
反照率贴图首先显示在标准着色器中。这是主要的纹理。它的属性位于properties数组内的某个位置。它的数组索引取决于在着色器中定义属性的顺序。但是按名称搜索它会更可靠。ShaderGUI包含FindProperty方法,该方法可以在给定名称和属性数组的情况下做到这一点。
除了texture属性,我们还需要定义标签的内容。这是通过GUIContent完成的,GUIContent是一个简单的容器类。
但是我们已经在着色器中将主要纹理命名为Albedo。所以我们只能使用该名称,可以通过属性访问该名称。
要创建这些小纹理小挂件,必须依赖已获得引用的编辑器。它具有绘制此类窗口小挂件的方法的集合。
(反照率贴图)
这开始看起来有点像标准着色器了!但是,当你将鼠标悬停在属性标签上时,该检查器也应该具有工具提示。对于反照率图,它表示反照率(RGB)和透明度(A)。
我们也可以通过简单地将其添加到标签内容中来添加工具提示。由于我们尚不支持透明度,因此我们仅使用Albedo(RGB)。
(反照率,有提示)
TexturePropertySingleLine方法的变体可与多个属性(最多三个)一起使用。第一个应该是纹理,其他的可以是其他东西。它们都将放在同一行。我们可以使用它在纹理旁边显示颜色。
(反照率和tint)
让我们跳到主要部分的底部。那就是显示主要纹理的平铺和偏移值的地方。这是通过MaterialEditor.TextureScaleOffsetProperty方法完成的。
(平铺和偏移)
1.4 便利方法
我们不使用现有的FindProperty方法,而是利用properties字段创建只需要一个name参数的方法。这将使代码更清晰。
在DoMain中切换为使用此方法。同样,我们可以直接将tint属性传递给TexturePropertySingleLine方法。因为不会在其他任何地方使用它。
再创建一种配置标签内容的方法。为此,我们只需要使用一个静态GUIContent实例来替换其文本和工具提示。由于我们可能一直不需要工具提示,因此将其设置为可选参数,并使用默认参数值。
假如我们不必一直烦恼需要从属性中提取显示名称,会更加方便。因此,创建一个可以做到这一点的MakeLabel变体。
现在,DoMain可以变得更小。我们所有未来的方法也是如此。
1.5 展示法线
下一个要显示的纹理是法线贴图。不要将所有代码都放在DoMain中,而是将其委托给单独的DoNormals方法。在反照率行之后,平铺和偏移之前调用它。
新的DoNormals方法只是检索map属性并显示它。标准着色器不提供任何额外的工具提示信息,因此我们也不会提供。
当然也有凹凸缩放,因此将其添加到行中。
(法线贴图 和凹凸比例)
当为材质指定了法线贴图时,标准着色器仅显示凹凸比例。也可以通过检查属性是否引用纹理来做到这一点。如果是这样的话,请显示凹凸比例。如果不是,则仅将null用作TexturePropertySingleLine的参数。
(隐藏 凹凸比例)
1.6 展示金属和平滑度
金属和光滑度特性是简单的浮动范围。最起码到现在。我们可以通过通用的MaterialEditor.ShaderProperty方法显示它们。与纹理方法不同,此方法将属性作为其第一个参数。标签内容排名第二。
(金属和平滑度)
通过增加编辑器的缩进级别,我们可以使这些属性与其他标签对齐。现在,分两个步骤进行。
通过静态EditorGUI.indentLevel属性调整缩进级别。之后请确保将其重置为旧值。
(缩进属性)
1.7 展示次要贴图
次要贴图的工作方式与主要贴图相同。因此,创建一个DoSecondary方法,该方法可以处理粗体标签,细节纹理及其平铺和偏移。
在我们的着色器中调整细节纹理的显示名称,以匹配标准着色器。
(次要贴图)
细节法线贴图的工作原理与主法线贴图相同。奇怪的是,标准着色器GUI不会隐藏细节凹凸比例。所以当没有细节法线贴图时我们应该将其隐藏。
(复合的检视器)
2 混合金属和非金属
因为我们的着色器使用统一的值来确定某种东西的金属性,所以它不能在材质的整个表面上变化。这使我们无法创建实际上代表不同材质混合的复杂材质。例如,这是计算机电路版的反照率图和法线图。
(电路的反照率图和法线图)
绿色部分构成电路板的底部,而蓝色部分代表光线。这些是非金属的。黄金部分代表导电电路,应为金属。最重要的是一些棕色污渍,有很多。
使用我们的照明着色器,用这些贴图创建新材质。使它相当平滑。另外,由于材质不是很亮,因此可以在Unity的默认环境下使用。如果场景的环境强度降低仍然是0的话,则将其设置为1。
(电路材质)
使用“Metallic ”滑块,我们可以使整个表面为非金属,金属或介于两者之间。但对于电路来说是不够的。
(统一值,非金属VS金属)
2.1 金属贴图
标准着色器支持金属贴图。这些贴图定义了每个纹理像素的金属值,而不是一次定义整个材质。这是一张灰度图,将电路标记为金属,其余标记为非金属。染色的金属较暗,因为其顶部为半透明的脏层。
(金属贴图)
将此类贴图的属性添加到我们的着色器里。
我们仍然需要NoScaleOffset属性吗?
这些属性是默认着色器GUI的提示。因此,我们不再需要它们。在本教程中一直使用它们作为提示,以帮助大家检查着色器代码。
也将相应的变量添加到我们的包含文件中。
创建一个函数,以插值器作为参数来检索片段的金属值。它只是对金属贴图进行采样,然后将其乘以统一的金属值。Unity使用贴图的R通道,因此我们也使用该通道。
现在我们可以在MyFragmentProgram中检索金属值。
请注意,MyFragmentProgram的代码并不关心如何获得金属值。如果要以其他方式确定金属值,则只需更改GetMetallic。
2.2 自定义GUI
如果我们仍然使用默认的着色器GUI,则金属贴图将出现在检查器中。但是现在我们必须通过调整DoMetallic将其显式添加到MyLightingShaderGUI中。像标准着色器一样,我们将贴图和滑块显示在一行上。
(使用金属贴图)
2.3 贴图还是滑块
使用金属贴图时,标准着色器的GUI隐藏滑块。我们也可以这样做。除了在没有纹理的情况下显示该值之外,它的作用类似于凹凸缩放。
(隐藏滑动条)
2.4 自定义着色器关键字
金属滑块被隐藏,因为标准着色器使用贴图或统一值。他们没有相乘。提供金属贴图时,将忽略统一值。要使用相同的方法,我们必须区分具有和不具有金属贴图的材质。这可以通过生成两个着色器变体来完成,一个带有映射,一个不带有映射。
由于着色器中的#pragma multi_compile指令,已经生成了多种着色器变体。它们基于Unity提供的关键字。通过定义自己的着色器关键字,我们可以创建所需的变体。
也可以按自己的喜好命名自定义关键字,但惯例是使用大写单词并带有下划线。现在,我们使用_METALLIC_MAP。
自定义关键字在哪里定义?
Unity基于多重编译语句以及将哪些关键字添加到材质中,来检测项目中的所有自定义关键字。在内部,它们被转换并组合为位掩码。关键字获得的标识符随项目而异。
在Unity 5.4中,位掩码包含128位。因此,每个项目最多可以存在128个关键字。这包括Unity的关键字以及所有正在使用的自定义关键字。该限制曾经较低,这使得具有许多关键字的着色器具有潜在的危害。Unity 5.5将限制增加到256。
要向材质添加自定义关键字,必须直接在GUI中访问该材质。可以通过MaterialEditor.target属性获取当前选择的材质。因为这实际上是从基本Editor类继承的属性,所以它具有通用的Object类型。因此,我们必须将其转换为Material。
使用Material.EnableKeyword方法将关键字添加到着色器中,该方法将关键字的名称作为参数。要删除关键字,请使用Material.DisableKeyword。让我们创建一个方便的函数,该函数基于布尔参数启用或禁用关键字。
现在,我们可以根据是否为_MetallicMap材质属性分配了纹理来切换自定义_METALLIC_MAP关键字。
2.5 调试关键字
可以使用调试检查器来验证我们的关键字是否已添加到材质中或从材质中删除。通过其选项卡栏右上方的下拉菜单将检查器切换到调试模式。自定义关键字在“Shader Keywords”文本字段中显示为列表。
(调试检视器)
由于以前在材质中分配了着色器,因此你在此处会找到的所有着色器关键字。例如,选择新材质后,标准着色器GUI就会添加_EMISSION关键字。它们对我们的着色器没有用,因此将其从列表中删除。
2.6 着色器特性
要生成着色器变体,我们必须向着色器添加另一个多重编译指令。对基本pass和附加pass都执行此操作。但阴影pass不需要它。
显示着色器变体时,你将看到已经包含我们的自定义关键字。现在,基本pass共有八个变体。
使用多重编译指令时,Unity会为所有可能的组合生成着色器变体。使用许多关键字时,编译所有排列可能会花费大量时间。所有这些变体也都包含在构建中,这可能是不必要的。
另一种方法是定义着色器功能,而不是多编译指令。区别在于着色器功能的排列仅在需要时才编译。如果没有材质使用某个关键字,则不会编译该关键字的着色器变体。Unity还检查在构建中使用了哪些关键字,仅包括必要的着色器变体。
因此,我们将#pragma shader_feature用作我们的自定义关键字。
什么时候可以使用着色器特性?
如果在设计时配置了材质(仅在编辑器中),则可以使用着色器功能而不必担心。但是,如果你在运行时调整材质的关键字,则必须确保包括所有变体。最简单的方法是对相关关键字坚持多编译指令。或者,你可以使用着色器变体集合资产。
如果着色器功能是单个关键字的切换,则可以省略单个下划线。
最后,在我们的包含文件中调整GetMetallic函数。定义_METALLIC_MAP后,对贴图进行采样。否则,返回统一值。
_MetallicMap或_Metallic二者只会使用一个吗?
是的。因此,材质将始终具有至少一种无用的属性。为了灵活性,这会产生一些开销。
2.7 仅在需要时设置关键字
目前,我们通常在每次调用OnGUI时都设置材质的关键字。从逻辑上讲,只有在map属性被编辑后,我们才需要这样做。可以使用EditorGUI.BeginChangeCheck和EditorGUI.EndChangeCheck方法检查是否有更改。第一种方法定义了我们要开始跟踪更改的点。第二种方法标记结束,并返回是否进行了更改。
通过在调用TexturePropertySingleLine之前和之后放置这些方法,我们可以轻松地检测出金属行是否已被编辑。如果是的话,我们设置关键字。
当_Metalic被更改时,这不也会触发吗?
是的,此代码在更改贴图和编辑统一值时都设置了关键字。这通常会很频繁,但仍然比一直都在要好得多。
这对撤消和重做有效吗?
是的。我们用来显示属性的MaterialEditor方法负责记录旧对象的状态。
3 平滑度贴图
像金属贴图一样,也可以通过贴图定义平滑度。这是一张电路的灰度平滑纹理。金属部分最光滑。其余部分相当粗糙。污渍比木板光滑,因此那里的纹理更浅。
(平滑度贴图)
Unity的标准着色器希望将平滑度存储在Alpha通道中。实际上,可以实现,金属贴图和平滑贴图在同一纹理中结合在一起。由于DXT5分别压缩了RGB和A通道,因此将贴图合并到一个DXT5纹理中将产生与使用两个DXT1纹理相同的质量。这并会减少内存,但是可以让我们从单个纹理样本(而不是两个)中同时获取金属和平滑度。
这是结合了两个贴图的纹理。尽管金属色只需要R通道,但我仍然用金属色值填充了RGB通道。平滑度使用Alpha通道。
(金属和平滑度贴图)
3.1 确定平滑度
当有金属贴图时,我们可以从它那里获得平滑度。否则,我们使用统一的_Smoothness属性。将GetSmoothness函数添加到我们的包含文件中以解决此问题。它几乎和GetMetallic一样。
是否仍会对纹理采样两次?
请记住,着色器编译器会删减重复的代码。我们在两个不同的函数中对同一纹理进行采样,但是编译后的代码将仅对纹理采样一次。我们不必显式的缓存这些内容。
实际上,标准着色器具有两个不同的平滑度属性。一是像我们一样的独立统一价值。另一个是调制后的平滑度贴图的标量。这里我们简单一些,也同时使用_Smoothness属性。这意味着必须将其设置为1才能获得未修改的平滑度贴图值。
使用此新函数可以在MyFragmentProgram中获得平滑度。
但这不是我们获取平滑值的唯一地方。CreateIndirectLight也使用它。我们可以在此函数中添加一个平滑度参数。但是也可以再次调用GetSmoothness。着色器编译器将检测重复的代码并对其进行优化。
(充分发挥贴图的平滑度)
沿着金属条边缘的那些正方形失真是什么?
这些失真是由法线贴图的DXT5nm纹理压缩引起的。太细的边无法正确估计,特别是如果它们未与UV轴对齐的时候。对于这种压缩,电路中尖锐的对角边缘是最坏的情况。在金属表面和非常光滑的表面上,此限制变得清晰可见。其他时候它没有那么明显。
(使用未压缩的法线贴图)
3.2 将平滑度与反照率相结合
当你同时需要将金属贴图和平滑贴图组合为单个纹理时,这很好。金属零件几乎总是比其他零件光滑。因此,当你需要金属贴图时,几乎总是需要光滑度贴图。但是如果你只需要一个平滑度贴图,而不要混合金属和非金属的时候,金属贴图是无用的。
对于不需要金属贴图的不透明材质,可以将平滑度存储在反照率贴图的Alpha通道中。由于这种做法很常见,因此标准着色器支持金属贴图或反照率贴图中的填充平滑度。我们也支持这一点。
3.3 不同关键字的切换
像标准着色器一样,我们必须添加一个选项来选择平滑度源到我们的GUI。尽管标准着色器仅支持在两个贴图之间进行选择,但我们可以扩展,添加一个统一的平滑度作为第三个选项。为了表示这些选项,请在MyLightingShaderGUI内部定义一个枚举类型。
当反照率贴图用作平滑度源时,将_SMOOTHNESS_ALBEDO关键字添加到材质中。使用金属源时,我们将添加_SMOOTHNESS_METALLIC。统一选项没有关键字。
标准着色器还使用float属性来跟踪材质使用的选项,但是我们可以单独使用关键字。GUI可以通过检查启用了哪个关键字来确定当前选择。这可以通过Material.IsKeywordEnabled方法完成,我们为此创建一个方便的包装器。
现在,DoSmoothness可以找出所选材质的当前平滑度来源。
为了显示这些选项,可以使用EditorGUILayout.EnumPopup方法。另外增加一个缩进级别,以匹配标准着色器的布局。
(平滑度源弹出窗口)
EnumPopup是一个基础的编辑器小部件,可为任何枚举创建一个弹出列表。它返回选择的值。如果用户未选择新选项,则该值与原始选项相同。否则,就有所不同。因此,要知道选择了哪个选项,我们必须将其分配回源变量。由于该方法适用于通用枚举类型,因此我们必须将其强制转换为SmoothnessSource。
如果进行了更改,我们可以使用source变量来控制应设置哪个关键字(如果有)。
(金属贴图的平滑度)
3.4 支持撤销
现在,我们可以更改平滑度源,但它尚不支持撤消和重做操作。因为我们使用的是基础小部件,所以必须手动发出信号,表明我们执行了需要支持撤消操作的操作。可以通过MaterialEditor.RegisterPropertyChangeUndo方法完成,该方法具有描述性标签作为参数。也为此方法创建一个包装器。
必须在要进行的更改之前调用RecordAction。它创建了旧状态的快照,因此撤消操作可以还原为旧状态。
3.5 平滑度变体
要支持所有三个选项,还要添加一个着色器功能,该功能可以在没有关键字_SMOOTHNESS_ALBEDO和_SMOOTHNESS_METALLIC之间进行选择。和以前一样,基础pass和附加pass都必须对其进行支持。
在GetSmoothness中,从1开始。然后检查是否选择了反照率源。如果是的话,则将1替换为反照率图。否则,检查是否选择了金属源,如果是,则使用金属图。当然,这仅在材质实际使用金属贴图时才有意义,因此也需要进行检查。
之后,返回得到的任何平滑度值乘以_Smoothness属性的值。如果我们最终得到一个不使用贴图的变体,则编译器将使用1来优化乘法。
3.6 岩浆材质
以下是反照率和法线贴图的示例,它们对冷却的岩浆产生了失真现象。该材质不是金属,但具有不同的平滑度。因此,平滑度值存储在反照率图的Alpha通道中。
(反照率带有平滑度,法线正常)
使用albedo source选项为平滑度创建带有这些贴图的材质。
(岩浆材质)
当使用反照率光源时,与红色带状沟相比,灰色的团块明显更光滑。
(使用反照率Alpha 统一值VS贴图)
4 自发光表面
到目前为止,我们仅处理了通过漫反射或镜面反射来反射光的材质。我们需要一个光源才能看到这样的表面。但是也有一些表面会自己发光。例如,当某物变得足够热时,它开始发光,不需要其他光源即可看到。标准着色器通过自发光贴图和颜色支持此操作,我们也这样做。
4.1 贴图和统一值
为我们的着色器添加自发光贴图和颜色的属性。默认情况下,两者都应为黑色,这意味着不发光。由于我们只关心RGB通道,还可以省略默认颜色的第四部分。
许多材质没有自发光贴图,因此让我们使用着色器功能创建不带有自发光贴图和带有自发光贴图的变体。因为我们只需要添加一次自发光,所以只需将特征包括在基本通道中即可。
将所需的采样器和float变量添加到包含文件中。
创建一个GetEmission函数以检索发出的颜色(如果有)。有贴图时,对其进行采样并乘以均匀的颜色。否则,只需返回均匀的颜色即可。但是只在基本pass中这么做。在所有其他情况下,emission 为零,编译器将对其进行优化。
由于发射光来自物体本身,因此它与反射光无关。只需将其添加到最终颜色即可。
4.2 把自发光添加到GUI
在MyLightingShaderGUI中创建DoEmission方法。最快的方法是复制DoMetallic并进行一些更改。
将其包括在主贴图部分中。
(检查器里带有自发光贴图和颜色)
4.3 HDR自发光
标准着色器不使用常规颜色进行自发光。相反,它支持高动态范围的颜色。这意味着该颜色的RGB分量可以高于1。这样,你就可以表示非常明亮的光。
我们可以看到比1亮的颜色吗?
在现实生活中,可以轰击的光子数量没有硬性限制。太阳非常明亮,令人眼花缭乱。但是,计算机显示受到限制。你不能高于1。其亮度取决于显示屏的亮度。
要有意义的使用HDR颜色,必须执行色调映射。这意味着你将一种颜色范围转换为另一种颜色范围。我们将在以后的教程中研究色调映射。HDR颜色通常也用于创建光晕效果。
由于颜色属性是浮点向量,因此我们不仅限于0–1范围内的值。但是,标准颜色挂件在设计时考虑了此限制。幸运的是,MaterialEditor包含TexturePropertyWithHDRColor方法,该方法专门用于纹理以及HDR颜色属性。它需要两个附加参数。首先,HDR范围的配置选项。其次,是否应该显示Alpha通道,这是我们不想要的。
通过ColorPickerHDRConfig对象配置HDR颜色小部件。该对象包含允许的亮度和曝光范围。标准着色器将0-99用作亮度,将0到3用于曝光。我们简单地使用相同的范围。
(具有HDR自发光的检视器)
颜色选择器后面的额外值与颜色的亮度相对应。这只是最大的RGB通道。将发光颜色切换为黑色或白色的快速方法是将此值设置为0或1。
4.4 自发光岩浆
这是岩浆材质的自发光图。它使沟壑中的熔岩炽热。你可以通过调整颜色来更改自发光的亮度和色调。
(岩浆的自发光贴图)
我分配了自发光图,但是没有显示?
出现那种情况的话,是因为统一值的自发光颜色仍为黑色。要以全强度查看贴图,请将颜色设置为白色。
如果在颜色为黑色的情况下指定了纹理,则标准着色器会自动将自发光颜色设置为白色。你也可以添加此功能。但是,该行为可能会导致某些情况下被强制修改,产生BUG。
(发光的岩浆,受光和不受光)
4.5 自发光电路
这是电路灯的自发光贴图。
(电路的自发光贴图)
灯光会影响亮度,污渍也会影响它们。
(电路具有正常工作的光照,受光VS不受光)
发出的光会照亮其他物体吗?
自发光仅是材质的一部分。它不会影响场景的其余部分。但是,Unity的全局照明系统可以拾取此发出的光并将其添加到间接照明数据中。我们将在以后的教程中研究全局照明。
下一章,更多的复合。