-
-
Save UXVirtual/ae4de5cff0709b0cb44448d8aa8755df to your computer and use it in GitHub Desktop.
Replaces trees on a terrain with prefab.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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