How do I globally (per project) replace/edit the builtin Standard PBR (Metallic/Specular) shaders with custom ones?

Hi there!

I would like to make just a few little changes to the builtin shaders and… my game is almost ready for launch. So there are tons of scenes and materials already using the default PBR Metallic/Specular Shaders.

I also have other materials/shaders as well, and they have to stay that way.

I’d like to replace:

  • builtin Standard PBR → custom Standard PBR
  • builtin Standard PBR (Specular) → custom Standard PBR (specular)

I’m targeting mobile, OpenGL ES 3.0, gamma color space, forward rendering, Subtractive mode lightmaps.

I found some info about replacement shaders, but I guess it’s not exactly what I’d love to obtain.

Is it even technically possible?

I’d like to avoid replacing over 100 of materials manually, unless you can suggest a tool to let me make such switch trivial (back and forth from builtin/custom)

I’m also open to other solutions which would allow to do basically the same.

as far as I can tell, you can’t update the builtin Standard shader, but you CAN update the builtin CGincludes in C:\Program Files\wherever your editor install is\Editor\Data\CGIncludes (that isn’t per-project though, it’s everything on that editor version.) If your shaders use the same properties as the standard shader, you could update the CGincludes referenced by the Standard shader (eg, “UnityStandardCoreForward.cginc” to do your thing instead, but tbh I can’t recommend doing something like this so close to shipping because it would probably initially break the hell out of all your other surface shaders (and possibly some of your frag shaders too!)

As far as mass-changing shaders on materials, here’s my route (simplified for space and readability). I have an Editor Utilities class where I keep editor API helper methods, and in that class I put

public static List<Material> GetMaterials()
    {
        var matFiles = GetFiles(GetSelectedPathOrFallback()).Where(s => s.Contains(".mat"));

        List<Material> toReturn = new List<Material>();
        foreach (string str in matFiles)
        {
            Material mat = (Material)AssetDatabase.LoadAssetAtPath(str, typeof(Material));
            if (mat != null && !toReturn.Contains(mat)) toReturn.Add(mat);
        }
        return toReturn;
    }

which returns a list of all materials in the currently selected directory in Assets. (if none is selected, it’ll run the search of ALL your assets.)

this uses a great utility method that I didn’t write,

//https://gist.github.com/kimsama/ff69cca140468f92d755
public static string GetSelectedPathOrFallback()
{
    string path = "Assets";

    foreach (UnityObject obj in Selection.GetFiltered(typeof(UnityObject), SelectionMode.Assets))
    {
        path = AssetDatabase.GetAssetPath(obj);
        if (!string.IsNullOrEmpty(path) && File.Exists(path))
        {
            path = Path.GetDirectoryName(path);
            break;
        }
    }
    return path;
}

Then have an editor window that creates a dictionary of the shaders you want to replace and the shaders you want to replace them with and use that to replace all the materials in the current directory. to avoid having half the answer be taken up by EditorGUI code, I’ll show a hardcoded dictionary, but ideally you’d actually get the shaders from object fields.

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.Linq;
public class ShaderStandardizer : EditorWindow
{
private Dictionary<Shader, Shader> toReplace = new Dictionary<Shader, Shader>();
        private bool initialized;
        [MenuItem("Tools/MyTools/Shader Replacer")]
        private static void OpenWindow()
        {
            GetWindow<ShaderStandardizer>().Show();
        }
        void InitializeDictionary()
        {
            if (!initialized)
            {
                toReplace.Add(Shader.Find("Standard"), Shader.Find("MyStudio/Category/ShaderName"));
                toReplace.Add(Shader.Find("Standard (Specular setup)"), Shader.Find("MyStudio/Category/OtherShaderName"));
                toReplace.Add(Shader.Find("Legacy Shaders/Bumped Specular"), Shader.Find("MyStudio/OtherCategory/ShaderName"));
                initialized = true;
            }
        }
        protected override void OnGUI()
        {
            InitializeDictionary();
            if (GUILayout.Button("Replace"))
            {
                string path = MyEditorUtils.GetSelectedPathOrFallback();
                if (path != "Assets" || EditorUtility.DisplayDialog(
                    "Search from root?",
                    "Searching all assets from root can potentially lock Unity. Proceed?",
                    "OK", "Cancel"))
                {
                    List<Material> materials = MyEditorUtils.GetMaterials().FindAll(m => toReplace.Keys.Contains(m.shader));
                    string title = string.Format("Update {0} materials?", materials.Count);
                    string message = string.Format("This operation will update {0} materials in {1} and subfolders. Proceed?", materials.Count, path);
                    string report = "UPDATED";
                    if (EditorUtility.DisplayDialog(title, message, "OK", "Cancel"))
                    {
                        foreach (Material material in materials)
                        {
                            report += string.Format("

{0} from {1} to {2}", AssetDatabase.GetAssetPath(material), material.shader.name, materials[material.shader].name);
material.shader = materials[material.shader];
}
}
//use System.IO to write the report to a text file or something
}
}
}
protected void OnDisable()
{
Resources.UnloadUnusedAssets();
}
}