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.