Search Unity

2019.2: Overriding shaders of Terrain-Grass

Discussion in 'Shaders' started by Selupsis, Aug 9, 2019.

  1. sirleto

    sirleto

    Joined:
    Sep 9, 2019
    Posts:
    146
    Hi Florian Alexandru / Toby Fredson,

    i tried your steps but it didnt work. what i did was downloading raw.githubusercontent.com/TwoTailsGames/Unity-Built-in-Shaders/master/DefaultResourcesExtra/TerrainShaders/Details/WavingGrass.shader

    then placed it in my assets folder. i edited the shader to show only RED color, but unitydid draw the grass as always (not red).

    unity 2021.3.5f1
    built-in
    default terrain with default grass

    i guess i missunderstood the steps you wrote, "all you need to do is have the shader in your project folder". where specifically? under a resources folder perhaps?

    thanks for any help!
     
  2. sirleto

    sirleto

    Joined:
    Sep 9, 2019
    Posts:
    146
  3. noio

    noio

    Joined:
    Dec 17, 2013
    Posts:
    230
    The method of creating a URP Asset subclass and overriding terrainDetailGrassBillboardShader used to work great!

    Right up until Unity 2023, because now if you do that you will get errors when inspecting a Volume Profile (a NullReference in UnityEditor.Help.IsUrl, strangely).

    So it's back to square one!

    [EDIT] I think I fixed this by.. adding a 'HelpUrl' attribute to the URP subclass:

    Code (CSharp):
    1. [HelpURL("http://example.com/docs/MyComponent.html")][/S][/S]
    2. [S][S][CreateAssetMenu(menuName = "Rendering/Universal Render Pipeline/URP with Custom Grass (Pipeline Asset)")][/S][/S]
    3. [S][S]public class URPWithCustomGrassShader : UniversalRenderPipelineAsset {[/S][/S]
    4. [S][S][/S][/S]
    5. [S][S]    public Shader detailGrassBillboardShader;[/S][/S]
    6. [S][S][/S][/S]
    7. [S][S]    public override Shader terrainDetailGrassBillboardShader => detailGrassBillboardShader;[/S][/S]
    8. [S][S]}[/S][/S]
    9. [S][S]


    Nope, it just hadn't reloaded yet.
     
    Last edited: Oct 10, 2023
  4. RaventurnPatrick

    RaventurnPatrick

    Joined:
    Aug 9, 2011
    Posts:
    250
    This never worked properly - for example shader graph and shader inspectors included in URP and HRDP use the following call to determine which gui to show: UnityEditor.ShaderUtil.GetCurrentCustomEditor
    This in turn uses
    GraphicsSettings.currentRenderPipeline?.GetType().FullName to check if it is URP or HDRP or a custom SRP - in case of a custom SRP a lot of the stuff does not properly work (such as shader graph)
     
  5. RaventurnPatrick

    RaventurnPatrick

    Joined:
    Aug 9, 2011
    Posts:
    250
    I think I found a potential solution, however I'm not sure if this will cover all cases - for the terrain grass it works:


    Code (CSharp):
    1. public static class UrpShaderReplacer
    2.   {
    3.     [DidReloadScripts]
    4.     [InitializeOnLoadMethod]
    5.     public static void InjectShaders()
    6.     {
    7.       Debug.Log("injecting shader into current pipeline");
    8.       var pipeline = GraphicsSettings.currentRenderPipeline;
    9.       if (pipeline == null)
    10.         return;
    11.      
    12.       var editorResourceAsset = pipeline.GetType()
    13.         .GetProperty("editorResources", BindingFlags.Instance | BindingFlags.NonPublic)
    14.         ?.GetValue(pipeline) as UniversalRenderPipelineEditorResources;
    15.  
    16.       if (editorResourceAsset == null)
    17.         return;
    18.  
    19.       // we replace the billboard shader
    20.       editorResourceAsset.shaders.terrainDetailGrassBillboardPS =
    21.         Shader.Find("Hidden/TerrainEngine/Details/UniversalPipeline/BillboardWavingDoublePassCustom");
    22.     }
    23.   }
    Now you just need to create a new shader in your project with the name in the shader.find block
    This works by reflection in the Unity Editor as it seems whatever shader is set in the current render pipeline will be used for runtime + builds + editor
     
    noio likes this.
  6. noio

    noio

    Joined:
    Dec 17, 2013
    Posts:
    230
    I'm going to try that out! Just setting the "terrainDetailGrassBillboardPS" property is a much more focused operation than subclassing the whole RenderPipeline..

    Still don't understand why this property isn't user accessible, would make things so much easier.

    I guess that Unity figures we couldn't figure out how data is provided to the Grass shaders since that's all undocumented..
    (which, they are right, it's extremely opaque what data gets sent to the terrain shaders, and how it's formatted.)
     
  7. RaventurnPatrick

    RaventurnPatrick

    Joined:
    Aug 9, 2011
    Posts:
    250
    Unfortunately in the build it does not work (only in editor) so we reverted back to the override solution.
    However for anyone that wants to use shader graph we found a very hacky fix for rendering the correct material editor with a custom render pipeline:

    This is the best way we found to solve the mess the unity team has created:
    However what I have seen there might be more systems negatively affected by the subclassing..

    Code (CSharp):
    1. using System;
    2. using System.Reflection;
    3. using UnityEditor;
    4. using UnityEngine;
    5. using UnityEngine.Rendering;
    6. using UnityEngine.Rendering.Universal;
    7.  
    8. namespace UnityEditor
    9. {
    10.   [CustomEditor(typeof (Material))]
    11.   [CanEditMultipleObjects]
    12.   public class CustomMaterialEditor : MaterialEditor
    13.   {
    14.     private MethodInfo _shouldEditorBeHidden;
    15.     private MethodInfo _checkSetup;
    16.     private MethodInfo _detectShaderEditorNeedsUpdate;
    17.     private MethodInfo _hasMultipleMixedShaderValues;
    18.     private MethodInfo _detectTextureStackValidationIssues;
    19.     private FieldInfo _mCustomEditorClassName;
    20.     private FieldInfo _mCustomShaderGUI;
    21.     private FieldInfo _mCheckSetup;
    22.     private FieldInfo _mShader;
    23.  
    24.     private void RefreshReflection()
    25.     {
    26.       if (_mCustomEditorClassName != null)
    27.         return;
    28.  
    29.       var baseType = GetType().BaseType.AssertNotNull();
    30.       const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
    31.       _shouldEditorBeHidden = baseType.GetMethod("ShouldEditorBeHidden", flags).AssertNotNull("ShouldEditorBeHidden");
    32.       _checkSetup = baseType.GetMethod("CheckSetup", flags).AssertNotNull("CheckSetup");
    33.       _detectShaderEditorNeedsUpdate = baseType.GetMethod("DetectShaderEditorNeedsUpdate", flags).AssertNotNull("DetectShaderEditorNeedsUpdate");
    34.       _hasMultipleMixedShaderValues = baseType.GetMethod("HasMultipleMixedShaderValues", flags).AssertNotNull("HasMultipleMixedShaderValues");
    35.       _detectTextureStackValidationIssues = baseType.GetMethod("DetectTextureStackValidationIssues", flags).AssertNotNull("DetectTextureStackValidationIssues");
    36.    
    37.       _mCustomEditorClassName = baseType.GetField("m_CustomEditorClassName", flags).AssertNotNull("m_CustomEditorClassName");
    38.       _mCustomShaderGUI = baseType.GetField("m_CustomShaderGUI", flags).AssertNotNull("m_CustomShaderGUI");
    39.       _mCheckSetup = baseType.GetField("m_CheckSetup", flags).AssertNotNull("m_CheckSetup");
    40.       _mShader = baseType.GetField("m_Shader", flags).AssertNotNull("m_Shader");
    41.     }
    42.  
    43.     public override void OnInspectorGUI()
    44.     {
    45.       var mat = (Material)target;
    46.       if (!mat || !mat.shader || !mat.shader.name.StartsWith("Shader Graphs"))
    47.       {
    48.         base.OnInspectorGUI();
    49.         return;
    50.       }
    51.  
    52.       // this class fixes a problem in Unity where subclassing the UniversalRenderPipelineAsset
    53.       // leads to the wrong material editor being displayed for shader graph shaders
    54.       if (GraphicsSettings.currentRenderPipeline == null ||
    55.           GraphicsSettings.currentRenderPipeline.GetType() != typeof(YourUrpPipelineAsset))
    56.       {
    57.         base.OnInspectorGUI();
    58.         return;
    59.       }
    60.  
    61.       RefreshReflection();
    62.  
    63.       // from here on it is a copy of base.OnInspectorGUI
    64.       // but we use reflection to avoid to much duplication as lot's of code is internal
    65.       if ((bool)_shouldEditorBeHidden.Invoke(this, Array.Empty<object>()))
    66.         return;
    67.  
    68.       serializedObject.Update();
    69.       _checkSetup.Invoke(this, Array.Empty<object>());
    70.       _detectShaderEditorNeedsUpdate.Invoke(this, Array.Empty<object>());
    71.       CreateCustomShaderEditorIfNeeded(((Material)target).shader);
    72.       if (isVisible
    73.           && _mShader.GetValue(this) != null
    74.           && !(bool)_hasMultipleMixedShaderValues.Invoke(this, Array.Empty<object>())
    75.           && PropertiesGUI())
    76.       {
    77.         foreach (var o in targets)
    78.         {
    79.           var m = (Material)o;
    80.           if (_mCustomShaderGUI.GetValue(this) != null)
    81.             ((ShaderGUI)_mCustomShaderGUI.GetValue(this)).ValidateMaterial(m);
    82.         }
    83.  
    84.         PropertiesChanged();
    85.       }
    86.  
    87.       _detectTextureStackValidationIssues.Invoke(this, Array.Empty<object>());
    88.     }
    89.  
    90.     private void CreateCustomShaderEditorIfNeeded(Shader shader)
    91.     {
    92.       RefreshReflection();
    93.       var currentCustomEditor = GetCurrentCustomEditor(shader);
    94.       if (string.IsNullOrEmpty(currentCustomEditor))
    95.       {
    96.         _mCustomEditorClassName.SetValue(this, "");
    97.         _mCustomShaderGUI.SetValue(this, null);
    98.       }
    99.       else
    100.       {
    101.         if ((string)_mCustomEditorClassName.GetValue(this) == currentCustomEditor)
    102.           return;
    103.  
    104.         _mCustomEditorClassName.SetValue(this, currentCustomEditor);
    105.         _mCustomShaderGUI.SetValue(this, CreateShaderGUI(currentCustomEditor));
    106.         _mCheckSetup.SetValue(this, true);
    107.       }
    108.     }
    109.  
    110.     private static string GetCurrentCustomEditor(Shader shader)
    111.     {
    112.       return shader == null
    113.         ? null
    114.         : ShaderUtil.GetCustomEditorForRenderPipeline(shader, typeof(UniversalRenderPipelineAsset));
    115.     }
    116.  
    117.     private static Type ExtractCustomEditorType(string customEditorName)
    118.     {
    119.       if (string.IsNullOrEmpty(customEditorName))
    120.         return null;
    121.       var str = $"UnityEditor.{customEditorName}";
    122.       foreach (var c in TypeCache.GetTypesDerivedFrom<ShaderGUI>())
    123.       {
    124.         var typeName = c.FullName.AssertNotNull();
    125.         if (typeName.Equals(customEditorName, StringComparison.Ordinal) ||
    126.             typeName.Equals(str, StringComparison.Ordinal))
    127.           return typeof(ShaderGUI).IsAssignableFrom(c) ? c : null;
    128.       }
    129.  
    130.       return null;
    131.     }
    132.  
    133.     private static ShaderGUI CreateShaderGUI(string customEditorName)
    134.     {
    135.       var customEditorType = ExtractCustomEditorType(customEditorName);
    136.       return customEditorType != null
    137.         ? Activator.CreateInstance(customEditorType) as ShaderGUI
    138.         : null;
    139.     }
    140.   }
    141. }
     
    adamgryu likes this.
  8. tw00275

    tw00275

    Joined:
    Oct 19, 2018
    Posts:
    92
    This worked great for me. If you are using a script to change URP settings that are normally private, such as https://github.com/inc8877/GraphicsConfigurator or https://forum.unity.com/threads/change-shadow-resolution-from-script.784793/page-2#post-8880891, then you'll need to access the base class instead.

    System.Type universalRenderPipelineAssetType = 
    (GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset).GetType();

    Change to:

    System.Type universalRenderPipelineAssetType = 
    (GraphicsSettings.currentRenderPipeline as CustomURP).GetType().BaseType;
     
  9. mmcclellanhyp

    mmcclellanhyp

    Joined:
    Mar 3, 2022
    Posts:
    1
    Another solution I verified works on URP in 2022.3.17 for at least editing the terrain waving grass shader is similar to Edwige's solution.

    I followed the short step from this Unity Support post:
    https://support.unity.com/hc/en-us/articles/10002130075796-How-can-I-modify-Built-in-Shaders

    That article boils down to this for URP/HDRP:

    If you use URP or HDRP, move the render pipeline package from “Library/PackageCache/” to the “Packages/” directory in your project folder.​

    I was able to add a return float4(0,0,0,1); to the fragment shader in WavingGrassPasses.hlsl and it successfully changed the grass in my scene to black.

    See the Unity Support article for the caveats and info on the BIRP.
     
    Last edited: Mar 28, 2024
  10. tw00275

    tw00275

    Joined:
    Oct 19, 2018
    Posts:
    92
    I finished the article I was writing on how to modify the terrain grass shader to look better from above and to support point filtering.

    The best solution for me was to create a GitHub repo with a copy of URP, which allows the grass billboard shader to be easily changed.

    @mmcclellanhyp Embedding the package works just as well, but with a GitHub repo it's easier to share your modified version of URP between multiple projects.
     
    sirleto likes this.