Material Inspector
Every Shader Script shader has a Material Inspector that looks and works like the render pipeline's own: on URP like URP's Lit or Unlit Inspector, on HDRP like HDRP's. It's organized in foldout sections:
| Section | Contents |
|---|---|
| Surface Options | The surface settings chosen per material: surface type, blending, render face, alpha clipping, and the pipeline's other settings. |
| Surface Inputs | The shader's material properties. |
| Custom sections | Properties grouped with a "Section/Label" display name, and sections added by a plugin. |
| Emission Inputs | The emission settings, when the shader has emission. |
| Advanced Options | GPU instancing, the render queue and the pipeline's advanced settings. |
Sections without content aren't shown.
Arranging properties
Without any code, you arrange the shader's properties with attributes:
- the order of declaration is the order in the Inspector;
[DisplayName("Section/Label")]moves a property into a section of its own;[Inline]puts a property on the line of the texture before it;[Header]and[Space]add headings and space;[HideInInspector]hides a property.
See Material properties for the full list.
Inspector plugins
For more, such as showing a property only when another one is enabled, drawing custom controls, or setting up the material when a property changes, write an Inspector plugin: a class derived from ShaderGuiPlugin, registered on the shader with [GuiPlugin].
// Editor/WaterGui.cs
using Ahilevich.ShaderScript;
public class WaterGui : ShaderGuiPlugin
{
public override void BuildGui(GuiPluginContext ctx)
{
ctx.Section(ctx.SurfaceInputsSection, d =>
{
d.DrawTexture("BaseMap", inline1: "BaseColor");
d.DrawTilingOffset("BaseMap");
d.DrawProperty("Smoothness");
});
ctx.Section("Waves", d =>
{
d.DrawProperty("WaveHeight");
if (ctx.FindProperty("WaveHeight")?.floatValue > 0)
d.DrawProperty("WaveSpeed");
});
}
}
// Water.cs
[Shader, GuiPlugin("WaterGui")]
public class Water : ShaderScript.Lit { ... }
Where to put a plugin
A plugin uses the Unity Editor API, so it must be in Editor code: an Editor folder, or an assembly definition for the Editor platform only. Register it with its full type name, including the namespace. The plugin must have a public parameterless constructor.
The same plugin serves all the render pipelines the shader supports. Don't put it in an assembly that exists only when one pipeline is installed, or the shader's Inspector shows an error on the other pipeline.
BuildGui
BuildGui describes what to draw, section by section, with ctx.Section(name, callback):
- A section is identified by its name. All the calls with the same name, from one or several places in your code, add to the same foldout, in the order they were made.
- The standard names are
ctx.SurfaceOptionsSection,ctx.SurfaceInputsSectionandctx.AdvancedOptionsSection. Any other name adds a custom foldout, placed after Surface Inputs and before Advanced Options, in the order of first use. - In a section that also has built-in controls, such as Surface Options, the plugin's content always comes after all of them.
- A registered section is shown even if its callback draws nothing. To hide a whole section, for example on opaque materials, decide before calling
ctx.Section, not inside the callback.
By default, BuildGui draws all the shader's properties, as a shader without a plugin would. To keep that layout and only add to it, call base.BuildGui(ctx) first. To lay out everything yourself, don't call it.
The callback receives a GuiPluginDrawer, with methods that draw properties by their name in the shader class:
| Method | Draws |
|---|---|
DrawProperty(name, label) |
A property with its default control. |
DrawTexture(name, label, inline1, inline2) |
A texture on a single line, optionally with one or two properties next to it. |
DrawTilingOffset(textureName) |
A texture's Tiling and Offset fields. |
DrawMinMaxSlider(minName, maxName, label, min, max) |
Two properties as a range slider. |
These methods skip properties the shader doesn't have. For anything else, use d.Editor, Unity's MaterialEditor, or EditorGUILayout directly.
Reading the material's state
GuiPluginContext gives access to the material, ctx.Material, and to its properties, ctx.FindProperty(name), which returns null when the shader has no such property. It also reads the surface settings the same way on both render pipelines, whether a setting is fixed by the shader or chosen by the material:
| Member | Returns |
|---|---|
IsTransparent |
Whether the surface is transparent. |
GetBlend() |
The blending mode. |
GetFace() |
The rendered faces. |
IsAlphaClipEnabled() |
Whether alpha clipping is on. |
IsTextureAssigned(name) |
Whether a texture is assigned. |
GetMaterialType(), GetRefractionModel(), IsClearCoatEnabled() |
HDRP only: the material type, refraction model and clear coat state. |
ApplyProperties
ApplyProperties(Material material) sets up the material itself: keywords, render queue and anything else a property needs to take effect.
public override void ApplyProperties(Material material)
{
// Keeps a hidden property in sync with a visible one, so that the shader doesn't compute it per pixel.
float speed = material.GetFloat("_WaveSpeed");
material.SetFloat("_WaveFrequency", speed * 2 * Mathf.PI);
}
It's also where a shader that uses a Custom mode, such as [ClearCoat(ClearCoatMode.Custom)], turns the feature's keyword on or off.
It runs after every change in the Inspector, and also without an Inspector: when Unity validates the material, and for every material of the shader after the shader is recompiled. So read the values from the material, not from the Inspector, and make sure that running it twice gives the same result. The keywords of [Toggle], [KeywordSwitch] and [TextureIsSet] properties are already up to date when it runs.
Good to know
- A new plugin instance is created every time it's used, so don't keep state in its fields.
- If the plugin type can't be found or created, the Inspector shows an error in place of the shader's properties.