This is the ninth part of a tutorial series about rendering. Last time, we added support for environmental maps. In this part we'll combine multiple textures to create complex materials. But before we get to that, we need a better GUI for our shader.

This tutorial was made with Unity 5.4.1f1.

The detail normal map works just like the main normal map. Curiously, the standard shader GUI doesn't hide the detail bump scale. But we're consistent, so we do hide it when there's no detail normal map.

Adjust the display name of the detail texture in our shader, to match the standard shader.

The secondary maps work just like the main maps. So create a DoSecondary method which takes care of the bold label, the detail texture, and its tiling and offset.

The indent level can be adjusted via the static EditorGUI.indentLevel property. Make sure to reset it to its old value afterwards.

We can make these properties line up with the other labels, by increasing the indent level of the editor. In this case, by two steps.

The metallic and smoothness properties are simple float ranges. At least, for now. We can show them via the general-purpose MaterialEditor.ShaderProperty method. Unlike the texture methods, this method has the property as its first argument. The label contents come second.

The standard shader only shows the bump scale when there is a normal map assigned to the material. We can do this too, by checking whether the property references a texture. If it does, show the bump scale. If not, just use null as an argument for TexturePropertySingleLine .

Of course there is a bump scale as well, so add it to the line.

The new DoNormals method simply retrieves the map property and displays it. The standard shader doesn't provide any extra tooltip info, so we won't either.

The next texture to be displayed is the normal map. Instead of putting all the code in DoMain , delegate it to a separate DoNormals method. Invoke it after the albedo line, before the tiling and offset.

Now DoMain can become even smaller. The same goes for all our future methods.

It's even more convenient if we don't have to bother with extracting the display name from properties all the time. So create a MakeLabel variant that does this as well.

Let's also create a method to configure the contents of a label. We only need to use a single static GUIContent instance for this. We'll just replace its text and its tooltip. As we might not need a tooltip all the time, let's make it optional, with a default parameter value.

Switch to using this method in DoMain . Also, we can directly pass the tint property to the TexturePropertySingleLine method. We're not using it anywhere else.

Instead of using the existing FindProperty method, let's create one that only requires a name parameter, taking advantage of our properties field. This will make our code more legible.

Let's skip ahead to the bottom of the main section. That's where the tiling and offset values of the main texture are shown. This is done with the MaterialEditor.TextureScaleOffsetProperty method.

The TexturePropertySingleLine method has variants that work with more than one property, up to three. The first should be a texture, but the others can be something else. They will all be put on the same line. We can use this to display the tint next to the texture.

We can add a tooltip as well, by simply adding it to the label content. As we don't support transparency yet, let's just use Albedo (RGB).

This is beginning to look like the standard shader! But that inspector also has tooltips, when you hover over the property labels. In the case of the albedo map, it says Albedo (RGB) and Transparency (A).

To create one of those small texture widgets, we have to rely on the editor that we've been given a reference to. It has a collection of methods to draw such widgets.

But we've already named the main texture Albedo in our shader. We can just use that name, which we can access via the property.

Besides the texture property, we also need to define the contents of a label. This is done with GUIContent , which is a simple container class.

The albedo map is shown first in the standard shader. This is the main texture. Its property sits somewhere inside the properties array. Its array index depends on the order in which the properties are defined in our shader. But it is more robust to search for it by name. ShaderGUI contains the FindProperty method, which does exactly that, given a name and a property array.

MaterialEditor decides when a new ShaderGUI instance is created. This currently happens when a material is selected, as you might expect. But it also happens when an undo or redo action is performed. This means that you cannot rely on a ShaderGUI instance sticking around. Each time, it could be a new object instance. You could think of OnGUI as if it were a static method, even though it isn't.

To show the properties of our material, we have to access them in our methods. We could pass the parameters of OnGUI on to all other methods, but this would lead to a lot of repeated code. Instead, let's put them in fields.

The standard shader has a bold label, so we want a bold label as well. This is done by adding a GUI style to the label, in this case EditorStyles.boldLabel .

Besides that, the EditorGUI and EditorGUILayout classes provide access to widgets and features for editor UIs.

The basis of the immediate-mode UI is the GUI class. It contains methods which create UI widgets. You have to use rectangles to position each element explicitly. The GUILayout class provides the same functionality, while automatically positioning the widgets using a simple layout system.

The Unity Editor is created with Unity's immediate-mode UI. This is Unity's old UI system, which was also used for in-game UIs before the current canvas-based system.

The standard shader GUI is split into two sections, one for the main maps, and another for the secondary maps. We'll use the same layout in our GUI. To keep the code clean, we'll use separate methods for distinct parts of the GUI. We start with the main section and its label.

Inside this method, we can create our own GUI. As we're not doing so yet, the inspector has become empty.

To replace the default inspector, we have to override the ShaderGUI.OnGUI method. This method has two parameters. First, a reference to a MaterialEditor . This object manages the inspector of the currently selected material. Second, an array containing that material's properties.

Yes. You have to specify the fully-qualified class name in the shader.

To use a custom GUI, you have to add the CustomEditor directive to a shader, followed by a string containing the name of the GUI class to use.

Under the hood, Unity uses the default material editor for shaders that have a custom ShaderGUI associated with them. This editor instantiates the GUI and invokes its methods.

Unity 4.1 added support for custom material inspectors, via extending MaterialEditor . You can still do this, but ShaderGUI was added as an alternative in 5.0. Its creation has something to do with Substance materials. Unity uses ShaderGUI for the standard shader, so we'll use it as well.

We can create a custom inspector by adding a class that extends UnityEditor.ShaderGUI . As it is an editor class, place its script file in an Editor folder.

Up to this points, we've been using Unity's default material inspector for our material. It is serviceable, but Unity's standard shader has quite a different look. Let's create a custom inspector for our own shader, mimicking the standard shader.

Mixing Metal and Nonmetal

Because our shader uses a uniform value to determine how metallic something is, it cannot vary across a material's surface. This prevents us from creating complex materials that actually represent a mix of different materials. For example, here are the albedo and normal maps for an artistic impression of computer circuitry.

Albedo and normal map for circuitry.

The green parts form the base of the circuit board, while the blue parts represent lights. These are nonmetallic. The yellow gold parts represent conductive circuitry, which should be metallic. On top of that are some brown stains, for variety.

Create a new material with these maps, using our lighting shader. Make it fairly smooth. Also, because the material isn't bright, it works with Unity's default ambient environment. So set the scene's Ambient Intensity back to 1, if you still had it lowered to zero.

Circuitry material.

Using the Metallic slider, we can make the whole surface either nonmetallic, metallic, or something in between. This is not sufficient for the circuitry.

Uniform nonmetal vs. metal.

Metallic Maps The standard shader has support for metallic maps. These maps define the metallic value per texel, instead of for the whole material at once. Here is a grayscale map which marks the circuitry as metallic, and the rest as nonmetallic. Stained metal is darker, because of the semitransparent dirty layer on top. Metallic map. Add a property for such a map to our shader. 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 } Do we still need the NoScaleOffset attributes? Those attributes are hints for the default shader GUI. So no, we don't need them anymore. I keep using them in this tutorial as hints for people inspecting the shader code. Add the corresponding variable to our include file as well. sampler2D _MetallicMap; float _Metallic; Let's create a function to retrieve the metallic value of a fragment, with the interpolators as a parameter. It simply samples the metallic map and multiplies it with the uniform metallic value. Unity uses the R channel of the map, so we use that channel as well. struct Interpolators { … }; float GetMetallic (Interpolators i) { return tex2D(_MetallicMap, i.uv.xy).r * _Metallic; } Now we can retrieve the metallic value in MyFragmentProgram . float4 MyFragmentProgram (Interpolators i) : SV_TARGET { … albedo = DiffuseAndSpecularFromMetallic( albedo, GetMetallic(i), specularTint, oneMinusReflectivity ); … } Note that the code of MyFragmentProgram doesn't care how the metallic value is obtained. If you want to determine the metallic value a different way, you only have to change GetMetallic .

Custom GUI Had we still used the default shader GUI, the metallic map would've appeared in the inspector. But now we have to explicitly add it to MyLightingShaderGUI , by adjusting DoMetallic . Like the standard shader, we show the map and the slider on a single line. void DoMetallic () { MaterialProperty map = FindProperty("_MetallicMap"); editor.TexturePropertySingleLine( MakeLabel(map, "Metallic (R)"), map, FindProperty("_Metallic") ); } Using a metallic map.

Map or Slider The GUI of the standard shader hides the slider when a metallic map is used. We can do so as well. It works like the bump scales, except that the value is shown when there's no texture. editor.TexturePropertySingleLine( MakeLabel(map, "Metallic (R)"), map, map.textureValue ? null : FindProperty("_Metallic") ); Hidden slider.

Custom Shader Keywords The metallic slider is hidden, because the standard shader uses either a map, or a uniform value. They aren't multiplied. When a metallic map is provided, the uniform value is ignored. To use the same approach, we have to distinguish between materials with and without a metallic map. This can be done by generating two shader variants, one with and one without the map. There are already multiple variants of our shader generated, due to the #pragma multi_compile directives in our shader. They're based on keywords provided by Unity. By defining our own shader keywords, we can create the variants that we need. You can name custom keywords however you like, but the convention is to use uppercase words with a leading underscore. In this case, we'll use _METALLIC_MAP. Where are custom keywords defined? Unity detects all custom keywords in a project, based on multi-compile statements and which keywords are added to materials. Internally, they are converted and combined into bit masks. Which identifier a keyword gets varies per project. In Unity 5.4, the bit mask contains 128 bits. Therefore, up to 128 keywords can exist per project. This includes Unity's keywords plus any custom keywords that are in use. This limit used to be lower, which made shaders with many keywords a potential hazard. Unity 5.5 will increase the limit to 256. To add custom keywords to a material, we have to access the material directly in our GUI. We can get to the currently selected material via the MaterialEditor.target property. As this is actually an inherited property from the base Editor class, it has the generic Object type. So we have to cast it to Material . Material target; MaterialEditor editor; MaterialProperty[] properties; public override void OnGUI ( MaterialEditor editor, MaterialProperty[] properties ) { this.target = editor.target as Material; this.editor = editor; this.properties = properties; DoMain(); DoSecondary(); } Adding a keyword to a shader is done with the Material.EnableKeyword method, which has the keyword's name as a parameter. For removal of a keyword, there's Material.DisableKeyword . Let's create a convenient method that enables or disables a keyword based on a boolean parameter. void SetKeyword (string keyword, bool state) { if (state) { target.EnableKeyword(keyword); } else { target.DisableKeyword(keyword); } } Now we can toggle our custom _METALLIC_MAP keyword, based on whether there's a texture assigned to the _MetallicMap material property. void DoMetallic () { … SetKeyword("_METALLIC_MAP", map.textureValue); }

Debugging Keywords You can use the debug inspector to verify that our keyword gets added to or removed from the material. You can switch the inspector to debug mode via the dropdown menu at the top right of its tab bar. The custom keywords are shown as a list in the Shader Keywords text field. Debug inspector. Any unexpected shader keywords you find here have been defined because of previous shaders that were assigned to the material. For example, as soon as you selected a new material, the standard shader GUI will add the _EMISSION keyword. They are useless to our shader, so remove them from the list.

Shader Features To generate the shader variants, we have to add another multi-compile directive to our shader. Do this for both the base pass and the additive pass. The shadow pass doesn't need it. #pragma target 3.0 #pragma multi_compile _ _METALLIC_MAP When showing the shader variants, you will see that our custom keyword has been included. The base pass now has a total of eight variants. // Total snippets: 3 // ----------------------------------------- // Snippet #0 platforms ffffffff: SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP 8 keyword variants used in scene: <no keywords defined> VERTEXLIGHT_ON SHADOWS_SCREEN SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP VERTEXLIGHT_ON _METALLIC_MAP SHADOWS_SCREEN _METALLIC_MAP SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP When using a multi-compile directive, Unity generates shader variants for all possible combinations. Compiling all permutations can take a lot of time, when many keywords are used. All these variants are also included in builds, which might be unnecessary. An alternative is to define a shader feature, instead of a multi-compile directive. The difference is that permutations of shader features are only compiled when needed. If no material uses a certain keyword, then no shader variants for that keyword are compiled. Unity also checks which keywords are used in builds, only including the necessary shader variants. So let's used #pragma shader_feature for our custom keyword. #pragma shader_feature _ _METALLIC_MAP When can you use shader features? When materials are configured at design time – in the editor only – then you can use shader features without worry. But if you adjust the keywords of materials at run time, then you have to make sure that all variants are included. The simplest way is to stick to multi-compile directives for the relevant keywords. Alternatively, you can use a shader variant collection asset. If the shader feature is a toggle for a single keyword, you can omit the single underscore. #pragma shader_feature _METALLIC_MAP After making this change, all shader variants are still listed, although the order in which Unity lists them might be different. // Total snippets: 3 // ----------------------------------------- // Snippet #0 platforms ffffffff: _METALLIC_MAP SHADOWS_SCREEN VERTEXLIGHT_ON 8 keyword variants used in scene: <no keywords defined> _METALLIC_MAP VERTEXLIGHT_ON VERTEXLIGHT_ON _METALLIC_MAP SHADOWS_SCREEN SHADOWS_SCREEN _METALLIC_MAP SHADOWS_SCREEN VERTEXLIGHT_ON SHADOWS_SCREEN VERTEXLIGHT_ON _METALLIC_MAP Finally, adjust the GetMetallic function in our include file. When _METALLIC_MAP is defined, sample the map. Otherwise, return the uniform value. float GetMetallic (Interpolators i) { #if defined(_METALLIC_MAP) return tex2D(_MetallicMap, i.uv.xy).r ; #else return _Metallic; #endif } So either _MetallicMap or _Metallic is used, never both? That is correct. So the material will always have at least one useless property. It's a little bit of overhead for the sake of flexibility.