Shade More Efficiently:
Taking Advantage of Multi-Compile Shaders
Target Audience: Intermediate devs, comfortable with both creating shaders, and scripting in Unity.
Implementation: Unity
Last Updated: Jan 2014
Note: All example scripts in this tutorial are written in c#, but the same effects can be accomplished in both java and boo.
Index:
The Big Idea
Multi-Compile shaders are something unity has used internally since the beginning, but has recently (as of version 4.1.0) made practical to use in high level surface shaders with the ability to extend the material editor class. What this allows you to do is define a feature that you would like to toggle on and off with a preprocessor directive like:
#pragma multi_compile FEATURE_OFF FEATURE_ON
And then in your shader, you can use a preprocessor conditional statement to dictate what to do if the feature is on or off.
o.Albedo = float3(0,0,1,1); #if FEATURE_ON // nonsensical feature example code here. o.Albedo += float3(1,0,0,0); #endif
What this will do is actually compile two shaders. If the material has "FEATURE_OFF" defined, it will use a shader that looks like this:
o.Albedo = float3(0,0,1,1);And if it has "FEATURE_ON" defined, it will use a shader that looks like this:
o.Albedo = float3(0,0,1,1); o.Albedo += float3(1,0,0,0);
So why would you want to do this? It means that instead of writing many seperate shaders for each specific case, you can write a few generalized, but customizable shaders, and re-use them throughout your project. You also gain a performance benefit over using one shader which may or may not be taking advantage of all of it's features (A shader with a fresnel effect set to 0 is still doing a fresnel calculation, and then discarding it).
A Basic Implementation
So let's apply this to something that we might actually want to do in a real world scenario, like enabling or disabling a normal map in our shader.
Shader "Multi-CompileTutorial/NormalToggle" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NormalMap ("Normal Map", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 300 CGPROGRAM #pragma surface surf Lambert #pragma multi_compile NORMALMAP_ON NORMALMAP_OFF sampler2D _MainTex; sampler2D _NormalMap; struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; #if NORMALMAP_ON o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); #endif } ENDCG } CustomEditor "NormalToggleInspector" }
This is a simple surface shader that samples from a diffuse and normal map, but the normal sample and unpacking has been put inside a precompiler if statement, so if NORMALMAP_ON is not defined, it won't sample from the normal map. Having no normal map assigned would have the same effect visually, but the shader would still do a texture lookup, and and run the UnpackNormal function.
Now we need to provide a means to toggle between NORMALMAP_ON and NORMALMAP_OFF. If you look at the second last line of the previous shader, you can see that it's referencing a custom editor. This is the name of the c# class that we want to use in place of unity's default material editor.
Note: This script needs to be inside a folder within your unity project named "Editor" for it to be recognized by unity as an editor script.
C#:
Now if you throw our newly created shader into a material, you should see a shiny new tickbox, and if you throw a normal map on, toggling it should enable and disable the normal map. While it appears that you're just turning the normal map on and off, you're actually flipping between two seperate compiled shaders, how neat is that!?
A More Flexible Implementation
While that's the implementation that's given in the unity documentation, it has some major drawbacks.
- Time consuming to add multiple feature toggles in the editor, since they need to be manually coded in.
- Implementing toggles for additional shaders requires copy-pasting code for the inspectors.
- All shader properties are shown at all times (even if the normal map is disabled, the texture property is still visible) making it unintuitive and possibly confusing for artists to use.
My solution was to create an abstract material editor base class that handles the grouping, and hiding of properties that are linked to a toggle automatically. What I wanted to accomplish was this:
- Group properties by what toggleable feature they are used with. (Normal map should display below the "Normal Enabled" check box.)
- Hide properties that are used with a toggleable feature if they are currently disabled. (Normal map property should not be visible if normals are not enabled.)
- Implement additional toggles and properties within existing material editors, and create new material editors for new shaders with minimal hassle.
The code below looks a bit hectic, but basically whats going on is this:
- Creating a list of "FeatureToggles", which represent a toggleable feature, and composed of:
- A name that will be displayed in the editor.
- A string that is used to determine which properties are owned by this toggle. (Normal Enabled might use the world "normal", so any properties that contain the world "normal" in their description would be owned by this toggle.)
- The keywords in the shader when this property is enabled or disabled (NORMALMAP_ON NORMALMAP_OFF)
- The whether the toggle is enabled or not.
- Draw all the shader properties that are not "owned" by a toggle.
- For each toggle, first draw the toggle, and then if the toggle is enabled, draw all the properties that it owns.
- If any toggles have been changed, compile a new array of keywords, using the _ON or _OFF version depending on the toggle's state.
C#:
Now, our Normal Toggle inspector will just look like this:
C#:
If you check out the inspector for our normal toggle shader, it should now show the normal map underneath our toggle, and if you uncheck it, it should hide.
Usage Examples
Here, I've expanded upon the normal toggle shader, and added toggleable specularity, fresnel, and rim lighting.
Shader "Multi-CompileTutorial/MoreToggles" { Properties { _DiffuseColor ("Diffuse Colour",color) = (1.0,1.0,1.0,1.0) _DiffuseMultiply ("Diffuse Brightness",float) = 1.0 _DiffuseMap ("Diffuse (RGB)", 2D) = "white" {} _NormalMap ("Normal Map(RGB)", 2D) = "bump" {} _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1) _SpecularMultiply ("Specular Brightness",float) = 1.0 _SpecAdd ("Specular Boost", float) = 0 _SpecMap ("Specular Map (RGB)", 2D) = "grey" {} _Gloss ("Specular Glossiness", float) = 0.5 _FresnelPower ("Fresnel Power",float) = 1.0 _FresnelMultiply ("Fresnel Multiply", float) = 0.2 _FresnelBias ("Fresnel Bias", float) = -0.1 _RimPower ("RimLight Power",float) = 1.0 _RimMultiply ("RimLight Multiply", float) = 0.2 _RimBias ("RimLight Bias", float) = 0 } SubShader { Tags { "RenderType"="Opaque" } LOD 300 CGPROGRAM #pragma surface surf BlinnPhong #pragma target 3.0 #pragma multi_compile NORMALMAP_ON NORMALMAP_OFF #pragma multi_compile SPECULAR_ON SPECULAR_OFF #pragma multi_compile FRESNEL_ON FRESNEL_OFF #pragma multi_compile RIMLIGHT_ON RIMLIGHT_OFF float3 _DiffuseColor; float _DiffuseMultiply; sampler2D _DiffuseMap; sampler2D _NormalMap; float _SpecularMultiply; float _SpecAdd; sampler2D _SpecMap; float _Gloss; float _FresnelPower; float _FresnelMultiply; float _FresnelBias; float _RimPower; float _RimMultiply; float _RimBias; struct Input { float2 uv_DiffuseMap; #if SPECULAR_ON float2 uv_SpecMap; #endif #if NORMALMAP_ON float2 uv_NormalMap; #endif #if FRESNEL_ON || RIMLIGHT_ON float3 viewDir; #endif }; void surf (Input IN, inout SurfaceOutput o) { o.Albedo.rgb = _DiffuseMultiply * _DiffuseColor.rgb * tex2D (_DiffuseMap, IN.uv_DiffuseMap); #if SPECULAR_ON o.Specular = _Gloss; o.Gloss = _SpecAdd + _SpecularMultiply * tex2D (_SpecMap, IN.uv_SpecMap); #endif #if NORMALMAP_ON o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); #endif #if FRESNEL_ON && SPECULAR_ON || RIMLIGHT_ON float facing = saturate(1.0 - max(dot( normalize(IN.viewDir.xyz), normalize(o.Normal)), 0.0)); #if FRESNEL_ON && SPECULAR_ON float fresnel = max(_FresnelBias + (1.0-_FresnelBias) * pow(facing, _FresnelPower), 0); fresnel = fresnel * o.Specular * _FresnelMultiply; o.Gloss *= 1+fresnel; #endif #if RIMLIGHT_ON float rim = max(_RimBias + (1.0-_RimBias) * pow(facing, _RimPower), 0); rim = rim * o.Specular * _RimMultiply; o.Albedo *= 1+rim; #endif #endif } ENDCG } CustomEditor "MoreTogglesInspector" }
And accompanying inspector:
C#:
The inspector interface will now look like this, and you can enable or disable the effects in any combination.
Keep in mind that unity will compile a shader for every possible combination of toggles. So one toggle will compile 2 shaders, while two toggles will compile 4, and ten toggles will compile to a whopping 1024 shader variants, so it's best to keep the toggles to broad features instead of individual settings.
Have some feedback? I'd love to hear it. Find out how to get a hold of me on the About Page.