2019.2: Overriding shaders of Terrain-Grass

Hi!

For my project I’d like to modify the shader that is used to render the grass-meshes that I paint on the terrain with the Unit tools. However, following the instructions I have found I still cannot modify the shader…

Here’s what I tried:

  • Downloaded the built-in shaders for the correct version
  • copied the shaders into the Assets-folder
  • Tried to modify the shader “Hidden/TerrainEngine/Details/WavingDoublePass”

However, the grass always renders the same, even if I e.g. change the color in the shader

I tried tips that I found, like hitting the “Refresh”-Button for the according Detail-Stencil in the according details setting, reloading the project, creating a new terrain… Neither the grass-shader, nor the VertexUnlit-shader that I can choose when creating a mesh-detail brush are affected…

I remember it working in a 2018.x-Version of Unity, did something change in the meantime?

I’m currently working in a LWRP-project, but I also created a quick “normal” 3D-test-project, and it also did not work there…

Did it ever find a solution? I have been trying to get grass to render in LWRP for about a week now with no luck

1 Like

Not sure about the details of the grass shader but the name “WavingDoublePass” is a clue for me.
LWRP only allows single pass shaders.

No luck so far, I’m thinking about implementing something on my own, since I don’t know if/when this will be fixed…

It would make sense - only the Vertex shader you can pick is a singlepass shader as far as I remember, and it too doesn’t work… Might be wrong though!

I’ll try a simple project with a 2018.x later to see if it works there - it’s not an option for me to jump back to an older version, but it might be a useful information for others

Sorry, I missed a part there - the grass doesn’t render at all for you with the standard shaders in LWRP? Because for me it renders at least with the default shaders

1 Like

Ok, I did a quick test with 2018.4.6f1, and there overwriting of shaders definitley works - the overwritten shader is definitley loaded as soon as I drag it into the project.

However, I’m now at the same stage as Aerial_Knight, as the shader does not work (grass is only rendered in pink and does not move), so perhaps there is really a problem with using multipass-shaders… I’ll try out a few things I have found

1 Like

Having same issue with Nature/Terrain/Standard

1 Like

Having this issue in 2019, the latest version, I think that overriding must have stopped working for some reason? Really need this feature!

Same in 2019.2, not using LWRP either. (And LWRP isn’t an option due to how many missing features it has compared to the built in pipeline)

Slight update, the issue appears to be linked to specific TerrainData assets. I exported the heightmap and splatmap from the terrain, and created a new terrain and imported the heightmap. Newly painted grass now uses the custom shader.

However if I duplicate the existing TerrainData, the issue still persists with the copy. So the TerrainData must internally cache it’s Detail shaders or something, and there appears to be no way to modify it from C#.

So I filed a bug report, you can go here and vote for it: Unity Issue Tracker - Custom shader doesn't override built-in terrain shader

Voted, thanks guys. Thanks @xdegtyarev

Looks like the bug report was closed, and a fix was added in the latest beta version (2019.3.b12). Though I haven’t had any luck getting it to work.

it’s still listed in Known Issues as Graphics - General: Custom shader doesn’t override built-in terrain shader (1193781)

Hello, the issue is not reproducible on our side anymore and it seems everything works fine. If the issue still persists, could you please define the exact steps needed to reproduce the issue?

Cheers,
Vita

You’re right, I completely misread :stuck_out_tongue:

@VitaSkr I’ll see if I can file a bug report and track down the last working Unity version (and if a SRP affects it). But I feel this more comes down to a feature request (being able to assign a custom grass shader/material to a terrain). It seems this only used to work by accident, since it was never an official feature but more of a hack.

@VitaSkr i’ve sent an email today with updated sample and more details

Marked as By-design :frowning: Unity Issue Tracker - Custom shader doesn't override built-in terrain shader

Really annoying… In case it can help, this is how I solved this issue in my asset store plugin, which required that grass replacement hack to work:

using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;


// Kinda hardcoded, but works rather well
public class TerrainDataHelperWindow : EditorWindow
{
    [MenuItem("Window/BendinGrass/Terrain Helper")]
    public static void Init()
    {
        var window = (TerrainDataHelperWindow)GetWindow(typeof(TerrainDataHelperWindow));
        window.titleContent = new GUIContent("TerrainData tool (Unity 2019.2.0+)");
        window.Show();
    }

    void OnGUI()
    {
        DrawTerrainConversion();
    }


    private void OnInspectorUpdate()
    {
        TerrainConversionTarget = Selection.activeGameObject?.GetComponent<Terrain>();
    }

    bool _showTerrainConversion;

    Terrain _conversionTarget;


    TerrainData _dataTarget;
  
    public TerrainData DataTarget
    {
        get
        {
            return _dataTarget;
        }

        set
        {
            if (_dataTarget != value)
            {
                _dataTarget = value;
            }
        }
    }

    public Terrain TerrainConversionTarget
    {
        get
        {
            return _conversionTarget;
        }

        set
        {
            _conversionTarget = value;

            if (_conversionTarget != null)
                DataTarget = _conversionTarget.terrainData;
            else
                DataTarget = null;
        }
    }


    public Object Grass, Billboard, VertexLit;

    public void ExtractShaderAssets(TerrainData data)
    {
        string json = EditorJsonUtility.ToJson(data);

        string grassProp = GetJsonProperty(json, "m_DetailMeshGrassShader");
        string billboardProp = GetJsonProperty(json, "m_DetailBillboardShader");
        string vertexLitProp = GetJsonProperty(json, "m_DetailMeshLitShader");

        Grass = GetAsset(grassProp);
        Billboard = GetAsset(billboardProp);
        VertexLit = GetAsset(vertexLitProp);
    }

    public Object GetAsset(string prop)
    {
        Regex fileReg = new Regex("\"fileID\":\\s*\\d+");
        Regex guidReg = new Regex("\"guid\":\\s*\"\\w+\"");

        var f = fileReg.Match(prop);
        var g = guidReg.Match(prop);

        long fileID = long.Parse(f.Value.Split(':')[1]);
        string guid = g.Value.Split(':')[1].Trim('"');

        return AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid), typeof(Object));
    }

    public void InjectAsset(ref string prop, string guid, long localID)
    {
        Regex fileReg = new Regex("\"fileID\":\\s*\\d+");
        Regex guidReg = new Regex("\"guid\":\\s*\"\\w+\"");

        var f = fileReg.Match(prop);
        string idString = f.Value;
        idString = idString.Split(':')[0] + ": " + localID.ToString();

        prop = prop.Remove(f.Index, f.Length).Insert(f.Index, idString);
  
        var g = guidReg.Match(prop);
        string guidString = g.Value;
        guidString = guidString.Split(':')[0] + ": \"" + guid + "\"";

        prop = prop.Remove(g.Index, g.Length).Insert(g.Index, guidString);
    }

    public string GetJsonProperty(string json, string property)
    {
        Regex reg = new Regex("\"" + property + "\"\\s*:\\s*\\{(.|\\n)*?\\}");
        var m = reg.Match(json);

        if (m.Success)
            return m.Value;
        return null;
    }

    public bool ConversionReplace = true;
    public bool OverwriteAsset = false;

    public Shader ReplacementShader;

    void DrawTerrainConversion()
    {
        EditorGUILayout.HelpBox("Due to changes made in Unity 2019.2.x, you will most likely run into issues using BendinGrass on already existing terrains. To avoid repainting, you can use this conversion tool to generate a new TerrainData asset." +
            "\nSelect your terrain in the inspector and click the Convert button. If you turn off Auto-assign, you will have to assign the new Terrain Data manually, either using the field below, or inspector debug mode.", MessageType.Warning);

        EditorGUILayout.HelpBox("Your original TerrainData assets will not be overwritten, but it will generate new ones.\nIt will overwrite the assets only if they are already in the conversion folder.\n\nConversion target folder: \n" + MAIN_FOLDER + CONVERT_FOLDER, MessageType.Info);

        //TerrainConversionTarget = (Terrain)EditorGUILayout.ObjectField("Convert Terrain", TerrainConversionTarget, typeof(Terrain), true);
      
        if (TerrainConversionTarget != null)
        {
            EditorGUILayout.HelpBox($"Currently selected: {TerrainConversionTarget.name} ({TerrainConversionTarget.terrainData.name})", MessageType.None);


            TerrainConversionTarget.terrainData = (TerrainData)EditorGUILayout.ObjectField("Assigned TerrainData", TerrainConversionTarget.terrainData, typeof(TerrainData), false);

            EditorGUILayout.LabelField($"Shaders from {DataTarget.name}", EditorStyles.boldLabel);

            EditorGUI.indentLevel++;
            EditorGUILayout.HelpBox("You can see or change what the current shaders for this TerrainData are.\nSet to null if you want to use defaults.\nYou have to convert to actually change them.", MessageType.None);
            Grass = EditorGUILayout.ObjectField("Grass Shader", Grass, typeof(Shader), false);
            Billboard = EditorGUILayout.ObjectField("Billboard Shader", Billboard, typeof(Shader), false);
            VertexLit = EditorGUILayout.ObjectField("VertexLit Shader", VertexLit, typeof(Shader), false);

            if (GUILayout.Button("Extract from TerrainData"))
                ExtractShaderAssets(DataTarget);

            if (GUILayout.Button("Set null (default)"))
            {
                Grass = null;
                Billboard = null;
                VertexLit = null;
            }


            EditorGUI.indentLevel--;

            ConversionReplace = EditorGUILayout.Toggle("Auto-assign new data", ConversionReplace);
            if (GUILayout.Button(ConversionReplace?"Update and assign": "Update"))
                RecreateTerrainData(TerrainConversionTarget);
        }


    }

    const string MAIN_FOLDER = "Assets/BendinGrass/";
    const string CONVERT_FOLDER = "ConversionTool";
    string output = null;

    public void RecreateTerrainData(Terrain target)
    {
        TerrainData source = target.terrainData;

        TerrainData newData = new TerrainData();
      
        CopyTerrainData(source, ref newData);
        string fullPath = MAIN_FOLDER + CONVERT_FOLDER;
      
        if (!AssetDatabase.IsValidFolder(fullPath))
            AssetDatabase.CreateFolder(MAIN_FOLDER.TrimEnd('/'), CONVERT_FOLDER);

        string sourcePath = AssetDatabase.GetAssetPath(source.GetInstanceID());
        bool overwrite = false;
        if (sourcePath.StartsWith(fullPath))
            overwrite = true;

        string asset = fullPath + "/" + source.name + (overwrite ? ".asset" : "_converted.asset");

        AssetDatabase.CreateAsset(newData, asset);

        if (ConversionReplace)
        {
            target.terrainData = newData;
            var coll = target.GetComponent<TerrainCollider>();
            if (coll)
                coll.terrainData = newData;
        }

        AssetDatabase.SaveAssets();
    }

    void CopyTerrainData(TerrainData source, ref TerrainData target)
    {
        // Quick way to create a deep copy of the source terrain data, with the updated shaders
        string toJson = EditorJsonUtility.ToJson(source, true);
        string fromJson = EditorJsonUtility.ToJson(target, true);

        CopyJsonProperty("m_DetailMeshGrassShader", ref fromJson, ref toJson, Grass);
        CopyJsonProperty("m_DetailMeshLitShader", ref fromJson, ref toJson, VertexLit);
        CopyJsonProperty("m_DetailBillboardShader", ref fromJson, ref toJson, Billboard);
        EditorJsonUtility.FromJsonOverwrite(toJson, target);
    }

    // "\w+"\s*:\s*\{(.|\n)*?\}
    void CopyJsonProperty(string property, ref string from, ref string to, Object injectObject = null)
    {
        Regex reg = new Regex("\"" + property + "\"\\s*:\\s*\\{(.|\\n)*?\\}");
        var fromMatch = reg.Match(from);
        var toMatch = reg.Match(to);

        if (fromMatch.Success)
        {
            if (toMatch.Success)
            {
                to = to.Remove(toMatch.Index, toMatch.Length);
                string fr = fromMatch.Value;
                if (injectObject!=null)
                {
                    string guid;
                    long id;
                    if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(injectObject, out guid, out id))
                        InjectAsset(ref fr, guid, id);
                }
                to = to.Insert(toMatch.Index, fr);
            }
        }
    }
}

It opens a window that lets you change the shaders saved inside the TerrainData asset. Probably not the most efficient way to do it, but does the job.

Left the instructions inside the code.

4 Likes