using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
namespace WSMGameStudio.TerrainTools
{
public static class WSM_TerrainTools
{
///
/// Backup terrain data that are affected by the terrain tools methods
///
///
public static void Backup(Terrain terrain)
{
WSM_TerrainBackup backup = terrain.GetComponent();
backup = backup == null ? terrain.gameObject.AddComponent() : backup;
backup.SaveBackup();
}
///
/// Restore last backup (if any)
/// Only data that is affected by the terrain tools methods are restored
///
///
public static void RestoreBackup(Terrain terrain)
{
WSM_TerrainBackup backup = terrain.GetComponent();
if (backup == null) return;
backup.LoadBackup();
}
///
///
///
///
///
///
///
public static void AdjustHeightAlongPath(Terrain terrain, Vector3[] path, int size, out List affectedCoordinates)
{
Profiler.BeginSample("AdjustHeightAlongPath");
//Normalize size to avoid zero width flat area
size = size + 1;
affectedCoordinates = new List();
string tempCoord = string.Empty;
Transform terrainTransform = terrain.GetComponent();
TerrainData terrainData = terrain.terrainData;
Vector3 localPathPoint;
Vector2 coordinates = Vector2.zero;
float[,] terrainHeights;
int xBase, yBase, width, height;
for (int pointIndex = 0; pointIndex < path.Length; pointIndex++)
{
// Convert spline points into terrain coordinates
localPathPoint = terrainTransform.InverseTransformPoint(path[pointIndex]);
ConvertPointToCoordinates(terrainData, localPathPoint, (terrainData.heightmapResolution - 1), (terrainData.heightmapResolution - 1), out coordinates);
CalculateAffectedAreaBoundaries(coordinates, size, terrainData.heightmapResolution, terrainData.heightmapResolution, out xBase, out yBase, out width, out height);
// Use terrainData.GetHeights() to retrieve current terrain heights
//The array has the dimensions[height, width] and is indexed as [y, x].
terrainHeights = terrainData.GetHeights(xBase, yBase, width, height);
// Calculate new height
int heightArrayLength = terrainHeights.GetLength(0);
int widthArrayLength = terrainHeights.GetLength(1);
for (int y = 0; y < heightArrayLength; y++)
{
for (int x = 0; x < widthArrayLength; x++)
{
terrainHeights[y, x] = CalculatePointHeight(terrainData, localPathPoint);
tempCoord = string.Format("{0}x{1}", xBase + x, yBase + y);
if (!affectedCoordinates.Contains(tempCoord))
affectedCoordinates.Add(tempCoord);
}
}
// Use terrainData.SetHeights/SetHeightsDelayLOD to apply the new heights
terrainData.SetHeightsDelayLOD(xBase, yBase, terrainHeights);
}
// Apply modification Use Terrain.ApplyDelayedHeightmapModification (Unity 2018) / TerrainData.SyncHeightmap (Unity 2019)
terrain.terrainData.SyncHeightmap();
Profiler.EndSample();
}
///
///
///
///
///
///
///
///
///
public static void SmoothenSlopeAlongPath(Terrain terrain, Vector3[] path, int size, float embankment, AnimationCurve embankmentSlope, List pathCoordinates)
{
if (embankment == 0)
return;
Profiler.BeginSample("SmoothenSlopeAlongPath");
//Normalize size to avoid zero width flat area
size += ((int)embankment * 2) + 1;
string tempCoord = string.Empty;
Transform terrainTransform = terrain.GetComponent();
TerrainData terrainData = terrain.terrainData;
Vector3 localPathPoint;
Vector2 coordinates = Vector2.zero;
float[,] terrainOriginalHeights = terrainData.GetHeights(0, 0, terrainData.heightmapResolution, terrainData.heightmapResolution);
float[,] terrainHeights;
int xBase, yBase, width, height;
for (int pointIndex = 0; pointIndex < path.Length; pointIndex++)
{
// Convert spline points into terrain coordinates
localPathPoint = terrainTransform.InverseTransformPoint(path[pointIndex]);
ConvertPointToCoordinates(terrainData, localPathPoint, (terrainData.heightmapResolution - 1), (terrainData.heightmapResolution - 1), out coordinates);
CalculateAffectedAreaBoundaries(coordinates, size, terrainData.heightmapResolution, terrainData.heightmapResolution, out xBase, out yBase, out width, out height);
// Use terrainData.GetHeights() to retrieve current terrain heights
//The array has the dimensions[height, width] and is indexed as [y, x].
terrainHeights = terrainData.GetHeights(xBase, yBase, width, height);
// Calculate new height
int heightArrayLength = terrainHeights.GetLength(0);
int widthArrayLength = terrainHeights.GetLength(1);
for (int y = 0; y < heightArrayLength; y++)
{
for (int x = 0; x < widthArrayLength; x++)
{
tempCoord = string.Format("{0}x{1}", xBase + x, yBase + y);
if (!pathCoordinates.Contains(tempCoord))
{
terrainHeights[y, x] = CalculateSlopeHeight(terrainData, terrainOriginalHeights, localPathPoint, embankmentSlope, x, y, widthArrayLength - 1, heightArrayLength - 1, embankment, xBase, yBase);
}
}
}
// Use terrainData.SetHeights/SetHeightsDelayLOD to apply the new heights
terrainData.SetHeightsDelayLOD(xBase, yBase, terrainHeights);
}
// Apply modification Use Terrain.ApplyDelayedHeightmapModification (Unity 2018) / TerrainData.SyncHeightmap (Unity 2019)
terrain.terrainData.SyncHeightmap();
Profiler.EndSample();
}
///
///
///
///
///
///
///
///
public static void PaintTextureAlongPath(Terrain terrain, Vector3[] path, Texture2D texture, int size, Texture2D embankmentTexture, float embankment, float minTextureBlending, float maxTextureBlending, float minEmbankmentTextureBlending, float maxEmbankmentTextureBlending)
{
if (texture == null)
return;
Profiler.BeginSample("PaintTextureAlongPath");
int paintedArea;
int strokeWidth, strokeHeight, texturesCount;
Transform terrainTransform = terrain.GetComponent();
TerrainData terrainData = terrain.terrainData;
Vector3 localPathPoint;
Vector2 coordinates = Vector2.zero;
float[,,] terrainAlphamap;
int xBase, yBase, width, height;
// Check if terrain does have corresponding texture
int textureIndex = FindTextureIndex(texture, terrainData);
if (textureIndex == -1)
{
Debug.LogWarning(string.Format("{0} - Corresponding terrain texture not found. Please make sure your terrain has a layer with the same diffuse map texture selected.", terrain.name));
Profiler.EndSample();
return;
}
/*
* Paint embankment texture
*/
int embankmentTextureIndex = FindTextureIndex(embankmentTexture, terrainData);
if (embankmentTextureIndex == -1)
{
Debug.LogWarning(string.Format("{0} - Corresponding Embankment terrain texture not found. Please make sure your terrain has a layer with the same diffuse map texture selected.", terrain.name));
}
else // Embankment Texture found
{
paintedArea = size + ((int)embankment * 2) + 1;
for (int pointIndex = 0; pointIndex < path.Length; pointIndex++)
{
// Convert spline points into terrain coordinates
localPathPoint = terrainTransform.InverseTransformPoint(path[pointIndex]);
ConvertPointToCoordinates(terrainData, localPathPoint, (terrainData.alphamapWidth - 1), (terrainData.alphamapHeight - 1), out coordinates);
//CalculateAffectedAreaBoundaries(coordinates, paintedArea, terrainData.alphamapWidth, terrainData.alphamapHeight, out xBase, out yBase, out width, out height);
CalculateAffectedAreaBoundaries(coordinates, paintedArea, terrainData.alphamapWidth, terrainData.alphamapHeight, out xBase, out yBase, out width, out height, true);
// Retrieve current textures "blending"
terrainAlphamap = terrainData.GetAlphamaps(xBase, yBase, width, height);
strokeWidth = terrainAlphamap.GetLength(0);
strokeHeight = terrainAlphamap.GetLength(1);
texturesCount = terrainAlphamap.GetLength(2);
// Calculate new textures alpha weights
for (int x = 0; x < strokeWidth; x++)
{
for (int y = 0; y < strokeHeight; y++)
{
SmoothTexturing(ref terrainAlphamap, embankmentTextureIndex, x, y, strokeWidth, strokeHeight, texturesCount, minEmbankmentTextureBlending, maxEmbankmentTextureBlending);
}
}
// Use terrainData.SetAlphamaps to apply the new texturing
terrainData.SetAlphamaps(xBase, yBase, terrainAlphamap);
}
}
/*
* Paint main texture
*/
paintedArea = size + 1;
for (int pointIndex = 0; pointIndex < path.Length; pointIndex++)
{
// Convert spline points into terrain coordinates
localPathPoint = terrainTransform.InverseTransformPoint(path[pointIndex]);
ConvertPointToCoordinates(terrainData, localPathPoint, (terrainData.alphamapWidth - 1), (terrainData.alphamapHeight - 1), out coordinates);
CalculateAffectedAreaBoundaries(coordinates, paintedArea, terrainData.alphamapWidth, terrainData.alphamapHeight, out xBase, out yBase, out width, out height);
// Retrieve current textures "blending"
terrainAlphamap = terrainData.GetAlphamaps(xBase, yBase, width, height);
strokeWidth = terrainAlphamap.GetLength(0);
strokeHeight = terrainAlphamap.GetLength(1);
texturesCount = terrainAlphamap.GetLength(2);
// Calculate new textures alpha weights
for (int x = 0; x < strokeWidth; x++)
{
for (int y = 0; y < strokeHeight; y++)
{
SmoothTexturing(ref terrainAlphamap, textureIndex, x, y, strokeWidth, strokeHeight, texturesCount, minTextureBlending, maxTextureBlending);
}
}
// Use terrainData.SetAlphamaps to apply the new texturing
terrainData.SetAlphamaps(xBase, yBase, terrainAlphamap);
}
Profiler.EndSample();
}
private static void SmoothTexturing(ref float[,,] terrainAlphamap, int targetTextureIndex, int x, int y, int strokeWidth, int strokeHeight, int texturesCount, float minTextureBlending, float maxTextureBlending)
{
float strongestTextureWeight = 0f;
int strongestTextureIndex = 0;
// Read texture weights, identify strongest texture
for (int t = 0; t < texturesCount; t++)
{
if (terrainAlphamap[x, y, t] > strongestTextureWeight)
{
strongestTextureWeight = terrainAlphamap[x, y, t];
strongestTextureIndex = t;
}
}
// Apply new texture weights
if (strongestTextureIndex == targetTextureIndex)
{
for (int t = 0; t < texturesCount; t++)
{
terrainAlphamap[x, y, t] = t == targetTextureIndex ? 1f : 0f;
}
}
else
{
float xWeight = x <= (strokeWidth * minTextureBlending) ? (x + 1) / (strokeWidth / 2f)
: (x >= strokeWidth * maxTextureBlending ? (strokeWidth - x + 1) / (strokeWidth / 2f) : 1f);
float yWeight = y <= (strokeHeight * minTextureBlending) ? (y + 1) / (strokeHeight / 2f)
: (y >= strokeHeight * maxTextureBlending ? (strokeHeight - y + 1) / (strokeHeight / 2f) : 1f);
float targetWeight = Mathf.Clamp01(xWeight * yWeight);
for (int t = 0; t < texturesCount; t++)
{
if (t == targetTextureIndex)
terrainAlphamap[x, y, t] = targetWeight;
else if (t == strongestTextureIndex)
terrainAlphamap[x, y, t] = 1f - targetWeight;
else
terrainAlphamap[x, y, t] = 0f;
}
}
}
///
/// Convert point to terrain coordinates
///
///
///
///
///
///
private static void ConvertPointToCoordinates(TerrainData terrainData, Vector3 localPathPoint, int mapWidth, int mapHeight, out Vector2 coordinates)
{
coordinates.x = (localPathPoint.x / terrainData.size.x) * mapWidth;
coordinates.y = (localPathPoint.z / terrainData.size.z) * mapHeight;
}
///
/// Calculate and validates affected area boundaries
///
///
///
///
///
///
///
///
///
private static void CalculateAffectedAreaBoundaries(Vector2 coordinates, int size, int mapWidth, int mapHeight, out int xBase, out int yBase, out int width, out int height, bool paintingBorders = false)
{
//Convert coordinates into array indexes
float offset = size / 2f;
xBase = paintingBorders ? Mathf.FloorToInt(coordinates.x - offset) : Mathf.CeilToInt(coordinates.x - offset);
yBase = paintingBorders ? Mathf.FloorToInt(coordinates.y - offset) : Mathf.CeilToInt(coordinates.y - offset);
width = (xBase < 0) ? (size + xBase) : size;
height = (yBase < 0) ? (size + yBase) : size;
//Expanding selection if smallest value (useful for borders)
width = paintingBorders ? width + 1 : width;
height = paintingBorders ? height + 1 : height;
// Validating map boundaries to prevent stack overflow
xBase = (xBase < 0) ? 0 : xBase;
yBase = (yBase < 0) ? 0 : yBase;
width = ((xBase + width) > mapWidth) ? (mapWidth - xBase) : width;
height = ((yBase + height) > mapHeight) ? (mapHeight - yBase) : height;
}
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
private static float CalculateSlopeHeight(TerrainData terrainData, float[,] terrainOriginalHeights, Vector3 localPathPoint, AnimationCurve embankmentSlope, int x, int y, int maxX, int maxY, float embankment, int xBase, int yBase)
{
float targetHeight = CalculatePointHeight(terrainData, localPathPoint);
float originalHeight = terrainOriginalHeights[yBase + y, xBase + x];
float currentHeight = terrainData.GetHeight(xBase + x, yBase + y);
currentHeight = currentHeight / terrainData.size.y;
bool originalHeightIsHeighter = originalHeight > targetHeight;
if (embankment <= 0)
return currentHeight;
float t = 1;
bool left = x < embankment;
bool right = x > (maxX - embankment);
bool top = y > (maxY - embankment);
bool down = y < embankment;
t = (left || down) ? (x < y ? (x / embankment) : (y / embankment)) : t;
t = (right || top) ? (x > y ? ((maxX - x) / embankment) : ((maxY - y) / embankment)) : t;
t = (left && top) ? (x <= (maxY - y) ? (x / embankment) : ((maxY - y) / embankment)) : t;
t = (right && down) ? ((maxX - x) <= y ? ((maxX - x) / embankment) : (y / embankment)) : t;
float slop = Mathf.Clamp01(embankmentSlope.Evaluate(t));
targetHeight = Mathf.Clamp01(Mathf.Lerp(originalHeight, targetHeight, slop));
if (originalHeightIsHeighter)
targetHeight = targetHeight > currentHeight ? currentHeight : targetHeight;
else
targetHeight = targetHeight > currentHeight ? targetHeight : currentHeight;
return targetHeight;
}
///
/// Converts point to terrain height value
///
///
///
///
private static float CalculatePointHeight(TerrainData terrainData, Vector3 localPathPoint)
{
float targetHeight = (localPathPoint.y / terrainData.size.y);
targetHeight = Mathf.Clamp01(targetHeight);
return targetHeight;
}
///
/// Returns corresponding terrain layer index. If not found, returns -1
///
///
///
///
private static int FindTextureIndex(Texture2D texture, TerrainData terrainData)
{
int textureIndex = -1;
if (texture == null)
return textureIndex;
for (int i = 0; i < terrainData.alphamapLayers; i++)
{
if (terrainData.terrainLayers[i].diffuseTexture == null)
continue;
if (texture.name == terrainData.terrainLayers[i].diffuseTexture.name)
{
textureIndex = i;
break;
}
}
return textureIndex;
}
///
/// Make sure terrain heightmap borders are "stitched" to neighbor terrains
///
///
public static void StitchBorders(Terrain terrain)
{
TerrainData terrainData = terrain.terrainData;
Terrain leftNeighbor = terrain.leftNeighbor;
Terrain bottomNeighbor = terrain.bottomNeighbor;
Terrain rightNeighbor = terrain.rightNeighbor;
Terrain topNeighbor = terrain.topNeighbor;
int resolution = terrainData.heightmapResolution;
float[,] edgeValues;
// Stitch this to leftNeighbor
if (leftNeighbor != null && terrainData.heightmapResolution == leftNeighbor.terrainData.heightmapResolution)
{
edgeValues = leftNeighbor.terrainData.GetHeights(resolution - 1, 0, 1, resolution); // Get left neighbor RIGHT border
terrainData.SetHeights(0, 0, edgeValues);
}
// Stitch rightNeighbor to this
if (rightNeighbor != null && terrainData.heightmapResolution == rightNeighbor.terrainData.heightmapResolution)
{
edgeValues = terrainData.GetHeights(resolution - 1, 0, 1, resolution); // Get this terrain RIGHT border
rightNeighbor.terrainData.SetHeights(0, 0, edgeValues);
}
// Stitch this to bottomNeighbor
if (bottomNeighbor != null && terrainData.heightmapResolution == bottomNeighbor.terrainData.heightmapResolution)
{
edgeValues = bottomNeighbor.terrainData.GetHeights(0, resolution - 1, resolution, 1); // Get bottom neighbor TOP border
terrainData.SetHeights(0, 0, edgeValues);
}
// Stitch topNeighbor to this
if (topNeighbor != null && terrainData.heightmapResolution == topNeighbor.terrainData.heightmapResolution)
{
edgeValues = terrainData.GetHeights(0, resolution - 1, resolution, 1); // Get this terrain TOP border
topNeighbor.terrainData.SetHeights(0, 0, edgeValues);
}
}
}
}