Skip to content

Instantly share code, notes, and snippets.

@UXVirtual
Forked from st4rdog/TreeReplacerS.cs
Last active April 6, 2025 03:23
Show Gist options
  • Save UXVirtual/ae4de5cff0709b0cb44448d8aa8755df to your computer and use it in GitHub Desktop.
Save UXVirtual/ae4de5cff0709b0cb44448d8aa8755df to your computer and use it in GitHub Desktop.
Replaces trees on a terrain with prefab.
using UnityEngine;
using UnityEditor;
// Replaces Unity terrain trees with prefab GameObjects.
// Handles terrain offsets correctly.
public class TreeReplacerS : EditorWindow
{
[Tooltip("The terrain containing the trees to replace.")]
public Terrain _terrain;
[Tooltip("The prefab to instantiate for each tree.")]
public GameObject _treePrefab; // Renamed for clarity
[Tooltip("Apply rotation from terrain tree instance?")]
public bool _applyRotation = true;
[Tooltip("Apply scale from terrain tree instance?")]
public bool _applyScale = true;
private const string GENERATED_CONTAINER_NAME = "TREES_GENERATED";
[MenuItem("Tools/Tree Replacer")]
static void Init()
{
TreeReplacerS window = (TreeReplacerS)GetWindow(typeof(TreeReplacerS), false, "Tree Replacer");
window.Show();
}
void OnGUI()
{
GUILayout.Label("References", EditorStyles.boldLabel);
_terrain = (Terrain)EditorGUILayout.ObjectField("Terrain", _terrain, typeof(Terrain), true);
_treePrefab = (GameObject)EditorGUILayout.ObjectField("Tree Prefab", _treePrefab, typeof(GameObject), false); // Allow only prefab assets
EditorGUILayout.Space();
GUILayout.Label("Settings", EditorStyles.boldLabel);
_applyRotation = EditorGUILayout.Toggle("Apply Rotation", _applyRotation);
_applyScale = EditorGUILayout.Toggle("Apply Scale", _applyScale);
EditorGUILayout.Space();
GUILayout.Label("Actions", EditorStyles.boldLabel);
// Disable buttons if references are missing
EditorGUI.BeginDisabledGroup(_terrain == null || _treePrefab == null);
if (GUILayout.Button("Convert (Keep Previous)", GUILayout.Height(30f)))
{
Convert(false); // Don't clear before converting
}
if (GUILayout.Button("Convert (Clear Previous)", GUILayout.Height(30f)))
{
Convert(true); // Clear before converting
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
// Disable clear buttons if terrain is missing
EditorGUI.BeginDisabledGroup(_terrain == null);
Color oldColor = GUI.backgroundColor;
GUI.backgroundColor = new Color(1.0f, 0.6f, 0.6f); // Light red
if (GUILayout.Button("Clear Generated Prefabs", GUILayout.Height(30f)))
{
ClearGeneratedPrefabs();
}
GUI.backgroundColor = oldColor; // Reset color
// Add a confirmation dialog for clearing terrain data trees as it's destructive
if (GUILayout.Button("Clear Terrain Tree Instances", GUILayout.Height(30f)))
{
if (EditorUtility.DisplayDialog("Confirm Clear Terrain Trees",
"This will permanently remove all tree instances from the terrain data itself. This cannot be undone easily.\nAre you sure?",
"Yes, Clear Terrain Trees", "Cancel"))
{
ClearTerrainTreeInstances();
}
}
EditorGUI.EndDisabledGroup(); // End disabled group for clear buttons
}
public void Convert(bool clearPrevious)
{
// --- Pre-checks ---
if (_terrain == null)
{
Debug.LogError("Tree Replacer: Terrain reference is missing!");
EditorUtility.DisplayDialog("Error", "Please assign a Terrain object.", "OK");
return;
}
if (_treePrefab == null)
{
Debug.LogError("Tree Replacer: Tree Prefab reference is missing!");
EditorUtility.DisplayDialog("Error", "Please assign a Tree Prefab.", "OK");
return;
}
// Ensure it's actually a prefab
if (PrefabUtility.GetPrefabAssetType(_treePrefab) == PrefabAssetType.NotAPrefab)
{
Debug.LogError("Tree Replacer: Assigned 'Tree Prefab' is not a valid prefab asset!");
EditorUtility.DisplayDialog("Error", "The assigned 'Tree Prefab' must be a Prefab Asset from your Project window, not an object from the scene.", "OK");
return;
}
TerrainData data = _terrain.terrainData;
if (data == null)
{
Debug.LogError("Tree Replacer: Terrain has no TerrainData!");
return;
}
// --- Clear if requested ---
if (clearPrevious)
{
ClearGeneratedPrefabs();
}
// --- Get or Create Parent Container ---
Transform parentTransform = _terrain.transform.Find(GENERATED_CONTAINER_NAME);
GameObject parentObject;
if (parentTransform == null)
{
parentObject = new GameObject(GENERATED_CONTAINER_NAME);
// Use Undo.RegisterCreatedObjectUndo for better editor integration
Undo.RegisterCreatedObjectUndo(parentObject, "Create Tree Container");
parentObject.transform.SetParent(_terrain.transform, false); // Set parent without affecting world position
parentObject.transform.localPosition = Vector3.zero; // Ensure it's at the terrain's origin
parentObject.transform.localRotation = Quaternion.identity;
parentObject.transform.localScale = Vector3.one;
}
else
{
parentObject = parentTransform.gameObject;
}
// --- Create Trees ---
TreeInstance[] trees = data.treeInstances; // Get a copy
if (trees.Length == 0)
{
Debug.LogWarning("Tree Replacer: No tree instances found on the terrain data.");
return;
}
Undo.SetCurrentGroupName("Convert Terrain Trees");
int undoGroup = Undo.GetCurrentGroup();
float terrainYBase = _terrain.transform.position.y; // Base Y position of the terrain object itself
for (int i = 0; i < trees.Length; i++)
{
TreeInstance tree = trees[i];
// --- Calculate Position ---
// 1. Get normalized position within terrain bounds
Vector3 normalizedPos = tree.position;
// 2. Scale by terrain size to get local position relative to terrain origin
Vector3 localPos = new Vector3(
normalizedPos.x * data.size.x,
normalizedPos.y * data.size.y, // This is height relative to terrain base height *at this point*
normalizedPos.z * data.size.z
);
// 3. (Optional but often needed) Sample terrain height if Y seems off
// float terrainHeightAtPoint = data.GetHeight((int)(normalizedPos.x * data.heightmapResolution), (int)(normalizedPos.z * data.heightmapResolution));
// localPos.y = terrainHeightAtPoint; // Use this if tree.position.y isn't giving correct world heights
// 4. Convert local terrain position to world position
Vector3 worldPosition = _terrain.transform.TransformPoint(localPos);
// --- Instantiate Prefab ---
GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(_treePrefab, parentObject.transform);
Undo.RegisterCreatedObjectUndo(go, "Instantiate Tree Prefab"); // Register for Undo
go.name = $"{_treePrefab.name}_{i}";
go.transform.position = worldPosition;
// --- Apply Rotation and Scale (Optional) ---
if (_applyRotation)
{
// TreeInstance.rotation is in radians
go.transform.rotation = Quaternion.Euler(0, tree.rotation * Mathf.Rad2Deg, 0); // Assuming Y rotation only
}
else
{
go.transform.rotation = Quaternion.identity; // Or keep prefab's default rotation
}
if (_applyScale)
{
go.transform.localScale = new Vector3(tree.widthScale, tree.heightScale, tree.widthScale);
}
else
{
go.transform.localScale = Vector3.one; // Or keep prefab's default scale
}
}
Undo.CollapseUndoOperations(undoGroup);
Debug.Log($"Tree Replacer: Successfully converted {trees.Length} trees.");
}
public void ClearGeneratedPrefabs()
{
if (_terrain == null)
{
Debug.LogWarning("Tree Replacer: No terrain assigned to clear generated prefabs from.");
return;
}
Transform parentTransform = _terrain.transform.Find(GENERATED_CONTAINER_NAME);
if (parentTransform != null)
{
// Use Undo.DestroyObjectImmediate for better editor integration
Undo.DestroyObjectImmediate(parentTransform.gameObject);
Debug.Log($"Tree Replacer: Destroyed '{GENERATED_CONTAINER_NAME}' under {_terrain.name}.");
}
else
{
Debug.LogWarning($"Tree Replacer: Could not find '{GENERATED_CONTAINER_NAME}' under {_terrain.name} to clear.");
}
}
public void ClearTerrainTreeInstances()
{
if (_terrain == null)
{
Debug.LogError("Tree Replacer: Terrain reference is missing!");
EditorUtility.DisplayDialog("Error", "Please assign a Terrain object.", "OK");
return;
}
if (_terrain.terrainData == null)
{
Debug.LogError("Tree Replacer: Terrain has no TerrainData!");
return;
}
// Register the change for Undo
Undo.RecordObject(_terrain.terrainData, "Clear Terrain Trees");
_terrain.terrainData.treeInstances = new TreeInstance[0]; // Assign empty array
// Important: Force terrain refresh in editor
_terrain.Flush();
EditorUtility.SetDirty(_terrain.terrainData); // Mark terrain data as changed
Debug.Log($"Tree Replacer: Cleared all tree instances from terrain data '{_terrain.terrainData.name}'.");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment