using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.TerrainTools
{
///
/// Exposes functions for making the best available use of the heightmap range on a given terrain
///
public static class OptimizeHeightRange
{
///
/// Performs RemapTerrainHeights on the currently selected Terrain with a target efficiency of 95%
///
/// Padding added below the height range; 0 - 1, relative the total height.
/// Padding added above the height range; 0 - 1, relative the total height.
public static void OptimizeSelectedTerrains(float padBottom = 0.025f, float padTop = 0.025f)
{
var selectedTerrains = SelectedContiguousTerrains(out bool isSelectedContiguous);
if (! isSelectedContiguous)
{
Debug.LogWarning("Choose one or more terrains from the same Terrain group before calling OptimizeSelectedTerrains");
return;
}
Undo.RegisterCompleteObjectUndo(GetUndoArray(selectedTerrains), "Optimize heightmap");
var terrainDatas= new List();
var bottom = float.MaxValue;
var range = float.MinValue;
foreach (var eachTerrain in selectedTerrains)
{
terrainDatas.Add(eachTerrain.terrainData);
bottom = Mathf.Min(eachTerrain.transform.position.y, bottom);
range = Mathf.Max(eachTerrain.terrainData.size.y, range);
}
GetTerrainHeightRange(out float minHeight, out float maxHeight, terrainDatas.ToArray());
var worldBottom = (minHeight * range) + bottom;
var worldTop = (maxHeight * range) + bottom;
var existingRange = worldTop - worldBottom;
var paddedBottom = Mathf.FloorToInt(worldBottom - (padBottom * existingRange));
var paddedTop = Mathf.CeilToInt(worldTop + (padTop * existingRange));
RemapSelectedTerrains(paddedBottom, paddedTop);
Debug.Log($"Optimized terrain heights to range {paddedBottom}-{paddedTop}");
}
///
/// Remap selected terrain objects to use the specified height range.
///
/// Unlike OptimizeSelectedTerrains this can be applied to multiple objects
///
/// The lower world-space bound of the intended height range
/// The upper world-space bound of the intended height range
public static void RemapSelectedTerrains(int minY, int maxY)
{
if (maxY <= minY)
{
Debug.LogWarning("Upper bound must be higher than lower bound");
return;
}
var selectedTerrains = SelectedContiguousTerrains(out bool isSelectedContiguous);
if (! isSelectedContiguous)
{
Debug.LogWarning("Select one or more terrains from the same Terrain group before calling RemapSelectedTerrains");
return;
}
Undo.RegisterCompleteObjectUndo(GetUndoArray(selectedTerrains), $"Remap Terrain Heights {minY}-{maxY}");
foreach (var eachObject in selectedTerrains)
{
RemapTerrainHeights(eachObject, minY, maxY);
}
Debug.Log($"Remap Terrain Heights to range {minY}-{maxY}");
}
///
/// Remap the heightmap pixels in a given Terrain so they completely fill a user-supplied range of heightmap values
/// while preserving their current world positions.
/// Heights outside the supplied range will be truncated to fit.
///
/// Terrain object to edit
/// The world Y coordinate of the new lower bound of the the target terrain
/// The world Y coordinate of the new upper bound of the the target terrain
public static void RemapTerrainHeights(Terrain targetTerrain, int heightMapMin, int heightMapMax)
{
var xform = targetTerrain.gameObject.transform;
var terrainData = targetTerrain.terrainData;
var oldPos = xform.position;
var originalPos = oldPos.y;
var oldScaleFactor = terrainData.heightmapScale.y;
var newRange = heightMapMax - heightMapMin;
if (newRange <= 0)
{
Debug.LogWarning("Remap range must be at least one unit");
return;
}
float Remapped(float h)
{
var hmapToWorld = originalPos + (oldScaleFactor * h);
return Mathf.Clamp01(Mathf.Max(0, hmapToWorld - heightMapMin) / newRange);
}
var stride = terrainData.heightmapResolution;
var existingHeights =
terrainData.GetHeights(0, 0, stride, stride);
for (var x = 0; x < stride; x++)
for (var y = 0; y < stride; y++)
existingHeights[x, y] = Remapped(existingHeights[x, y]);
terrainData.SetHeights(0, 0, existingHeights);
terrainData.size = new Vector3(terrainData.size.x, newRange, terrainData.size.z);
oldPos.y = heightMapMin;
targetTerrain.transform.position = oldPos;
}
///
/// Returns the minimum and maximum world height values for
///
/// . The results are
/// passed as out parameters.
///
///
/// float (will be reset)
/// float (will be reset)
/// One or more Terrain object
public static void GetTerrainHeightRange(out float minHeight, out float maxHeight, params TerrainData[] terrainDatas)
{
minHeight = float.MaxValue;
maxHeight = float.MinValue;
foreach (var eachData in terrainDatas)
{
foreach (var eachExtent in eachData.GetPatchMinMaxHeights())
{
minHeight = Mathf.Min(minHeight, eachExtent.min);
maxHeight = Mathf.Max(maxHeight, eachExtent.max);
}
}
}
///
/// Returns the selected contiguous Terrains as an array.
///
/// If the there are no selected terrains or the selection includes terrains with
/// more than one groupingID, the out parameter will be false.
///
///
///
internal static Terrain[] SelectedContiguousTerrains(out bool success)
{
var selectedTerrains = Selection.GetFiltered(SelectionMode.Deep);
success = selectedTerrains.Length == 1;
if (selectedTerrains.Length < 2)
{
return selectedTerrains;
}
int groupID = selectedTerrains[0].groupingID;
success = true;
foreach (var eachTerrain in selectedTerrains)
{
success = success && eachTerrain.groupingID == groupID;
}
return selectedTerrains;
}
internal static Object[] GetUndoArray(in Terrain[] terrains)
{
List