(Case 1094991)(Bump) Custom Shader GUI makes scene editing impossible

Hi. Unity 2019.3b includes some improvement for iterating editor works, like domain reload related features.

But I found that this simple bug (or by bad design) is not fixed even it significantly impact to editor iteration time. (In my project, it added 3~5sec to every single click of object. Scene-editing is almost impossible!)

Surprisingly, it is reported at 2018.3b! and it seems that Unity completely forgotten this issue so I bumped this to the forum.

I have this somewhat annoying workaround in our MaterialEditor for some time now which simply caches the custom editor string → type. Would be a very easy fix by Unity.

public override void OnEnable()
{
    if (!target)
        return;

    var shaderProperty = serializedObject.FindProperty("m_Shader");
    var shader = shaderProperty.objectReferenceValue as Shader;

    // prevent unity from creating custom shader gui as it's very slow
    shaderProperty.objectReferenceValue = null;
    base.OnEnable();
    shaderProperty.objectReferenceValue = shader;

    // create custom shader gui
    CreateCustomShaderEditorIfNeededFast(shader);
}

void CreateCustomShaderEditorIfNeededFast(Shader shader)
{
    var internalThis = new InternalClass(this, typeof(UnityEditor.MaterialEditor));
    var customEditor = string.Empty;
    if (shader != null)
    {
        var internalShader = new InternalClass(shader);
        customEditor = internalShader.Get<string>("customEditor");
    }

    if (shader == null || string.IsNullOrEmpty(customEditor))
    {
        internalThis.Set("m_CustomEditorClassName", "");
        internalThis.Set("m_CustomShaderGUI", null);
    }
    else
    {
        if (internalThis.Get<string>("m_CustomEditorClassName") == customEditor)
            return;
        internalThis.Set("m_CustomEditorClassName", customEditor);
        internalThis.Set("m_CustomShaderGUI", CreateShaderGUI(customEditor));
        internalThis.Set("m_CheckSetup", true);
    }
}

static Dictionary<string, Type> customEditorCache = new Dictionary<string, Type>();

static Type ExtractCustomEditorType(string customEditorName)
{
    if (string.IsNullOrEmpty(customEditorName))
        return null;

    if (customEditorCache.TryGetValue(customEditorName, out var editorType))
        return editorType;

    var str = "UnityEditor." + customEditorName;
    var loadedAssemblies = InternalClass.GetStatic<Assembly[]>("EditorAssemblies.loadedAssemblies");
    for (var index = loadedAssemblies.Length - 1; index >= 0; --index)
    {
        foreach (var c in GetTypesFromAssembly(loadedAssemblies[index]))
        {
            if (c.FullName.Equals(customEditorName, StringComparison.Ordinal) || c.FullName.Equals(str, StringComparison.Ordinal))
                editorType = !typeof(ShaderGUI).IsAssignableFrom(c) ? null : c;
        }
    }
    customEditorCache[customEditorName] = editorType;
    return editorType;
}

static Type[] GetTypesFromAssembly(Assembly assembly)
{
    if (assembly == null)
        return Array.Empty<Type>();
    try
    {
        return assembly.GetTypes();
    }
    catch (ReflectionTypeLoadException)
    {
        return Array.Empty<Type>();
    }
}

static ShaderGUI CreateShaderGUI(string customEditorName)
{
    var customEditorType = ExtractCustomEditorType(customEditorName);
    return customEditorType == null ? null : Activator.CreateInstance(customEditorType) as ShaderGUI;
}
2 Likes

@julian-moschuering Thanks for sharing code! I re-wrote your code for using Unity’s new TypeCache and replacing InternalClass type (maybe your own helper class?).

[CustomEditor(typeof(Material))]
public class MaterialEditorExtended : MaterialEditor
{
    private static Dictionary<string, Type> ShaderGUINameToType = new Dictionary<string, Type>();

    public override void OnEnable()
    {
        if(!target)
            return;

        var shaderProperty = serializedObject.FindProperty("m_Shader");
        var shader = shaderProperty.objectReferenceValue as Shader;

        // prevent unity from creating custom shader gui as it's very slow
        shaderProperty.objectReferenceValue = null;
        base.OnEnable();
        shaderProperty.objectReferenceValue = shader;

        // create custom shader gui
        CreateCustomShaderEditorIfNeededFast(shader);
    }

    void CreateCustomShaderEditorIfNeededFast(Shader shader)
    {
        string customEditor = GetCustomEditorClassName(shader);

        if (shader == null || string.IsNullOrEmpty(customEditor))
        {
            SetMaterialEditorFieldValue(this, "m_CustomEditorClassName", "");
            SetMaterialEditorFieldValue(this, "m_CustomShaderGUI", null);
            return;
        }
        string m_CustomEditorClassName = (string)GetMaterialEditorFieldValue(this, "m_CustomEditorClassName");

        if (m_CustomEditorClassName == customEditor)
            return;

        SetMaterialEditorFieldValue(this, "m_CustomEditorClassName", customEditor);
        SetMaterialEditorFieldValue(this, "m_CustomShaderGUI", CreateShaderGUI(customEditor));
        SetMaterialEditorFieldValue(this, "m_CheckSetup", true);
    }

    private ShaderGUI CreateShaderGUI(string customEditorClassName)
    {
        if (string.IsNullOrEmpty(customEditorClassName))
        {
            return null;
        }

        string editorPrefixedClassName = "UnityEditor." + customEditorClassName;

        if (!ShaderGUINameToType.TryGetValue(customEditorClassName, out var type))
        {
            type = TypeCache.GetTypesDerivedFrom<ShaderGUI>()
                .Where(t => t.FullName.Equals(customEditorClassName, StringComparison.Ordinal) || t.FullName.Equals(editorPrefixedClassName, StringComparison.Ordinal))
                .FirstOrDefault();

            if (type == null)
            {
                return null;
            }
            else
            {
                ShaderGUINameToType[customEditorClassName] = type;
            }
        }

        return Activator.CreateInstance(type) as ShaderGUI;
    }

    private static void SetMaterialEditorFieldValue(object instance, string name, object value)
    {
        typeof(MaterialEditor).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(instance, value);
    }

    private static object GetMaterialEditorFieldValue(object instance, string name)
    {
        return typeof(MaterialEditor).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(instance);
    }

    private static string GetCustomEditorClassName(Shader shader)
    {
        return typeof(Shader).GetProperty("customEditor", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(shader) as string;
    }
}
4 Likes

Hi, I submitted a similar fix to Unity 2020.1. We could backport it up too 2019.2 if needed.

Here’s a profiler screenshot that shows the memory and speed improvements (before and after):

4931303--478286--image.png

Thanks,

5 Likes

@jonathans42 Thanks for resolution. I think that this fix should absolutely be backported to 2019.x series.

The interface not running at seconds-per-frame rate is needed, yes.

Could you by any chance send us your project via a bug report (referencing the original case would suffice as a description)? We don’t have a reproducible that suffers this severely from the issue.

The same goes for everyone else. Additional reproduction projects would be much appreciated!

@LeonhardP My original report has minimul reproducible project. Case 1180416.

It has many dummy types and test object, renderer with multiple material. If you open that project and select object in hierarchy view, editor freeze 0.5~1 sec (depend on your machine spec). If the scene is playing, freeze time be much longer.

Currently, my production project applied hotfix inspired with @julian-moschuering

1 Like

The issue is of course, most noticeable in a project that has tons of textures and materials, custom shaders, etc. This means that to repro it most people would have to upload their real projects with all the different materials, textures, shaders etc. This suggests that the algorithms are such that they are not constant but scale with input size so repro projects with a few items probably work just fine

The custom script here helped a ton we can actually edit materials again (2019.2). Thanks! Please don’t leave 2019 without this fix as it will make the LTS release actual cancer since well, companies using the LTS release are obviously going to be making larger and larger projects with more and more assets and will start running into this problem.

@jonathans42 's fix has landed in Trunk and should be backported to 2019.2 and 2019.3 soon.

[quote=“Kichang-Kim, post:8, topic: 756931, username:Kichang-Kim”]
My original report has minimul reproducible project. Case 1180416.
[/quote]QA had a look at the project and the issue no longer reproduces with Jonathan’s fix.

2 Likes

It’s February of 2020, and I am using Unity 2019.3.0f6 and this is still happening to me. Can anyone tell me where to put that script you guys are using to fix this?

Could you please submit a bug report with a minimal reproduction project for the issue you’re experiencing?