Search Unity

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

Discussion in '2019.3 Beta' started by Kichang-Kim, Sep 3, 2019.

  1. Kichang-Kim

    Kichang-Kim

    Joined:
    Oct 19, 2010
    Posts:
    309
    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!)

    https://issuetracker.unity3d.com/issues/hdrp-editor-is-lagging-when-modifying-material-properties

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

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    329
    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.
    Code (CSharp):
    1.        
    2. public override void OnEnable()
    3. {
    4.     if (!target)
    5.         return;
    6.  
    7.     var shaderProperty = serializedObject.FindProperty("m_Shader");
    8.     var shader = shaderProperty.objectReferenceValue as Shader;
    9.  
    10.     // prevent unity from creating custom shader gui as it's very slow
    11.     shaderProperty.objectReferenceValue = null;
    12.     base.OnEnable();
    13.     shaderProperty.objectReferenceValue = shader;
    14.  
    15.     // create custom shader gui
    16.     CreateCustomShaderEditorIfNeededFast(shader);
    17. }
    18.  
    19. void CreateCustomShaderEditorIfNeededFast(Shader shader)
    20. {
    21.     var internalThis = new InternalClass(this, typeof(UnityEditor.MaterialEditor));
    22.     var customEditor = string.Empty;
    23.     if (shader != null)
    24.     {
    25.         var internalShader = new InternalClass(shader);
    26.         customEditor = internalShader.Get<string>("customEditor");
    27.     }
    28.  
    29.     if (shader == null || string.IsNullOrEmpty(customEditor))
    30.     {
    31.         internalThis.Set("m_CustomEditorClassName", "");
    32.         internalThis.Set("m_CustomShaderGUI", null);
    33.     }
    34.     else
    35.     {
    36.         if (internalThis.Get<string>("m_CustomEditorClassName") == customEditor)
    37.             return;
    38.         internalThis.Set("m_CustomEditorClassName", customEditor);
    39.         internalThis.Set("m_CustomShaderGUI", CreateShaderGUI(customEditor));
    40.         internalThis.Set("m_CheckSetup", true);
    41.     }
    42. }
    43.  
    44. static Dictionary<string, Type> customEditorCache = new Dictionary<string, Type>();
    45.  
    46. static Type ExtractCustomEditorType(string customEditorName)
    47. {
    48.     if (string.IsNullOrEmpty(customEditorName))
    49.         return null;
    50.  
    51.     if (customEditorCache.TryGetValue(customEditorName, out var editorType))
    52.         return editorType;
    53.  
    54.     var str = "UnityEditor." + customEditorName;
    55.     var loadedAssemblies = InternalClass.GetStatic<Assembly[]>("EditorAssemblies.loadedAssemblies");
    56.     for (var index = loadedAssemblies.Length - 1; index >= 0; --index)
    57.     {
    58.         foreach (var c in GetTypesFromAssembly(loadedAssemblies[index]))
    59.         {
    60.             if (c.FullName.Equals(customEditorName, StringComparison.Ordinal) || c.FullName.Equals(str, StringComparison.Ordinal))
    61.                 editorType = !typeof(ShaderGUI).IsAssignableFrom(c) ? null : c;
    62.         }
    63.     }
    64.     customEditorCache[customEditorName] = editorType;
    65.     return editorType;
    66. }
    67.  
    68. static Type[] GetTypesFromAssembly(Assembly assembly)
    69. {
    70.     if (assembly == null)
    71.         return Array.Empty<Type>();
    72.     try
    73.     {
    74.         return assembly.GetTypes();
    75.     }
    76.     catch (ReflectionTypeLoadException)
    77.     {
    78.         return Array.Empty<Type>();
    79.     }
    80. }
    81.  
    82. static ShaderGUI CreateShaderGUI(string customEditorName)
    83. {
    84.     var customEditorType = ExtractCustomEditorType(customEditorName);
    85.     return customEditorType == null ? null : Activator.CreateInstance(customEditorType) as ShaderGUI;
    86. }
    87.  
     
    Kichang-Kim likes this.
  3. Kichang-Kim

    Kichang-Kim

    Joined:
    Oct 19, 2010
    Posts:
    309
    @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?).

    Code (CSharp):
    1. [CustomEditor(typeof(Material))]
    2. public class MaterialEditorExtended : MaterialEditor
    3. {
    4.     private static Dictionary<string, Type> ShaderGUINameToType = new Dictionary<string, Type>();
    5.  
    6.     public override void OnEnable()
    7.     {
    8.         if(!target)
    9.             return;
    10.  
    11.         var shaderProperty = serializedObject.FindProperty("m_Shader");
    12.         var shader = shaderProperty.objectReferenceValue as Shader;
    13.  
    14.         // prevent unity from creating custom shader gui as it's very slow
    15.         shaderProperty.objectReferenceValue = null;
    16.         base.OnEnable();
    17.         shaderProperty.objectReferenceValue = shader;
    18.  
    19.         // create custom shader gui
    20.         CreateCustomShaderEditorIfNeededFast(shader);
    21.     }
    22.  
    23.     void CreateCustomShaderEditorIfNeededFast(Shader shader)
    24.     {
    25.         string customEditor = GetCustomEditorClassName(shader);
    26.  
    27.         if (shader == null || string.IsNullOrEmpty(customEditor))
    28.         {
    29.             SetMaterialEditorFieldValue(this, "m_CustomEditorClassName", "");
    30.             SetMaterialEditorFieldValue(this, "m_CustomShaderGUI", null);
    31.             return;
    32.         }
    33.         string m_CustomEditorClassName = (string)GetMaterialEditorFieldValue(this, "m_CustomEditorClassName");
    34.  
    35.         if (m_CustomEditorClassName == customEditor)
    36.             return;
    37.  
    38.         SetMaterialEditorFieldValue(this, "m_CustomEditorClassName", customEditor);
    39.         SetMaterialEditorFieldValue(this, "m_CustomShaderGUI", CreateShaderGUI(customEditor));
    40.         SetMaterialEditorFieldValue(this, "m_CheckSetup", true);
    41.     }
    42.  
    43.     private ShaderGUI CreateShaderGUI(string customEditorClassName)
    44.     {
    45.         if (string.IsNullOrEmpty(customEditorClassName))
    46.         {
    47.             return null;
    48.         }
    49.  
    50.         string editorPrefixedClassName = "UnityEditor." + customEditorClassName;
    51.  
    52.         if (!ShaderGUINameToType.TryGetValue(customEditorClassName, out var type))
    53.         {
    54.             type = TypeCache.GetTypesDerivedFrom<ShaderGUI>()
    55.                 .Where(t => t.FullName.Equals(customEditorClassName, StringComparison.Ordinal) || t.FullName.Equals(editorPrefixedClassName, StringComparison.Ordinal))
    56.                 .FirstOrDefault();
    57.  
    58.             if (type == null)
    59.             {
    60.                 return null;
    61.             }
    62.             else
    63.             {
    64.                 ShaderGUINameToType[customEditorClassName] = type;
    65.             }
    66.         }
    67.  
    68.         return Activator.CreateInstance(type) as ShaderGUI;
    69.     }
    70.  
    71.     private static void SetMaterialEditorFieldValue(object instance, string name, object value)
    72.     {
    73.         typeof(MaterialEditor).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(instance, value);
    74.     }
    75.  
    76.     private static object GetMaterialEditorFieldValue(object instance, string name)
    77.     {
    78.         return typeof(MaterialEditor).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(instance);
    79.     }
    80.  
    81.     private static string GetCustomEditorClassName(Shader shader)
    82.     {
    83.         return typeof(Shader).GetProperty("customEditor", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(shader) as string;
    84.     }
    85. }
     
  4. jonathans42

    jonathans42

    Unity Technologies

    Joined:
    Jan 25, 2018
    Posts:
    173
    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):

    image.png

    Thanks,
     
  5. Kichang-Kim

    Kichang-Kim

    Joined:
    Oct 19, 2010
    Posts:
    309
    @jonathans42 Thanks for resolution. I think that this fix should absolutely be backported to 2019.x series.
     
  6. hatless

    hatless

    Joined:
    Dec 15, 2010
    Posts:
    35
    The interface not running at seconds-per-frame rate is needed, yes.
     
  7. LeonhardP

    LeonhardP

    Unity Technologies

    Joined:
    Jul 4, 2016
    Posts:
    1,511
    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!
     
  8. Kichang-Kim

    Kichang-Kim

    Joined:
    Oct 19, 2010
    Posts:
    309
    @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 's script. Only freezing occurred at first time clicking, and subsequent clicking is blazing fast. Our designers are happy now.
     
    LeonhardP likes this.
  9. zornor90

    zornor90

    Joined:
    Sep 16, 2015
    Posts:
    163
    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.
     
    Last edited: Sep 7, 2019
  10. LeonhardP

    LeonhardP

    Unity Technologies

    Joined:
    Jul 4, 2016
    Posts:
    1,511
    @jonathans42 's fix has landed in Trunk and should be backported to 2019.2 and 2019.3 soon.
    QA had a look at the project and the issue no longer reproduces with Jonathan's fix.
     
    Peter77 and Kichang-Kim like this.