using System;
namespace UnityEngine.TerrainTools
{
internal class Heightmap
{
public enum Flip
{
None,
Horizontal,
Vertical,
};
public enum Mode
{
Global,
Batch,
Tiles
};
public enum Depth
{
Bit8 = 1,
Bit16 = 2
};
public enum Format
{
PNG,
TGA,
RAW
};
public readonly Vector2Int Size;
public int Width => Size.x;
public int Height => Size.y;
public float Remap = 1.0f;
public float Base = 0f;
private readonly float[,] m_NormalisedHeights;
private void FlipHeightsInPlaceHorizontally()
{
int otherX = Width - 1;
for (int x = 0; x < Width / 2; x++, otherX--)
{
for (int y = 0; y < Height; y++)
{
float temp = m_NormalisedHeights[y, x];
m_NormalisedHeights[y, x] = m_NormalisedHeights[y, otherX];
m_NormalisedHeights[y, otherX] = temp;
}
}
}
private void FlipHeightsInPlaceVertically()
{
int otherY = Height - 1;
for (int y = 0; y < Height / 2; y++, otherY--)
{
for (int x = 0; x < Width; x++)
{
float temp = m_NormalisedHeights[y, x];
m_NormalisedHeights[y, x] = m_NormalisedHeights[otherY, x];
m_NormalisedHeights[otherY, x] = temp;
}
}
}
public void FlipHeightsInPlace(Flip flip)
{
switch (flip)
{
case Flip.None:
{
break;
}
case Flip.Horizontal:
{
FlipHeightsInPlaceHorizontally();
break;
}
case Flip.Vertical:
{
FlipHeightsInPlaceVertically();
break;
}
default:
{
throw new ArgumentOutOfRangeException(nameof(flip), flip, null);
}
} // End of switch.
}
private static void ConvertInt8ToRawData(int value, byte[] buffer, int bufferIndex)
{
buffer[bufferIndex] = (byte)(value & 0xFF);
}
private static void ConvertInt16ToRawData(int value, byte[] buffer, int bufferIndex)
{
buffer[bufferIndex] = (byte)(value & 0xFF);
buffer[bufferIndex + 1] = (byte)((value >> 8) & 0xFF);
}
///
/// Converts the specified height-map to a 16-bit raw image file.
///
/// The array of bytes to be used.
public byte[] ConvertToRawData()
{
const int valueSize = 2;
const float scale = (1 << (valueSize * 8)) - 1;
int bufferSize = Width * Height * valueSize;
byte[] buffer = new byte[bufferSize];
int bufferIndex = 0;
for (int y = 0; y < Height; y++)
{
for (int x = 0; x < Width; x++, bufferIndex += valueSize)
{
float normalisedHeight = m_NormalisedHeights[y, x];
float scaledHeight = normalisedHeight * scale;
int value = Mathf.RoundToInt(scaledHeight);
ConvertInt16ToRawData(value, buffer, bufferIndex);
}
}
return buffer;
}
private static int ConvertRawDataToInt8(byte[] buffer, int bufferIndex)
{
int value = buffer[bufferIndex];
return value;
}
private static int ConvertRawDataToInt16(byte[] buffer, int bufferIndex)
{
int value = (buffer[bufferIndex + 1] << 8) | buffer[bufferIndex];
return value;
}
private static int[] ConvertFromRawData(byte[] rawData, int valueSize, Func convert)
{
int numBytes = rawData.Length;
int numValues = numBytes / valueSize;
int[] values = new int[numValues];
int index = 0;
for (int i = 0; i < numBytes; i += valueSize)
{
values[index++] = convert(rawData, i);
}
return values;
}
private static float[,] CalculateHeightData(byte[] rawData, int valueSize, Func convert, Vector2Int size)
{
int[] rawValues = ConvertFromRawData(rawData, valueSize, convert);
float[,] heightData = new float[size.y, size.x];
float scale = (1 << (valueSize * 8)) - 1;
float oneOverScale = 1.0f / scale;
int index = 0;
for (int y = 0; y < size.y; y++)
{
for (int x = 0; x < size.x; x++)
{
int rawValue = rawValues[index++];
float height = rawValue * oneOverScale;
heightData[y, x] = height;
}
}
return heightData;
}
private float[,] GenerateExactHeightDataFromParent(Heightmap parentHeightmap, Vector2Int offset, Vector2Int size)
{
float[,] parentHeightData = parentHeightmap.m_NormalisedHeights;
float[,] normalisedHeights = new float[size.y, size.x];
// Copy the height data from our parent height-map...
for (int y = 0; y < size.y; y++)
{
for (int x = 0; x < size.x; x++)
{
int parentX = offset.x + x;
int parentY = offset.y + y;
float parentHeight = parentHeightData[parentY, parentX];
m_NormalisedHeights[y, x] = parentHeight;
}
}
return normalisedHeights;
}
private float[,] GenerateBlendedHeightDataFromParent(Heightmap parentHeightmap, Vector2Int offset, Vector2Int size)
{
float[,] normalisedHeights = new float[size.y + 1, size.x + 1];
Vector2 parentOffset = new Vector2(0.0f, offset.y); // - 0.5f);
int width = size.x;
int height = size.y;
// Copy the height data from our parent height-map...
for (int y = 0; y <= height; y++, parentOffset.y++)
{
parentOffset.x = offset.x; // - 0.5f;
for (int x = 0; x <= width; x++, parentOffset.x++)
{
float parentHeight = parentHeightmap.GetNormalisedHeightAt(parentOffset);
normalisedHeights[y, x] = parentHeight * Remap + Base;
}
}
return normalisedHeights;
}
public Heightmap(byte[] rawData, Flip flip)
{
int numElements = rawData.Length;
int size = (int)Mathf.Sqrt(numElements);
int size2 = size * size;
// Is this an 8-bit raw image?
if (size2 == numElements)
{
Size = new Vector2Int(size, size);
m_NormalisedHeights = CalculateHeightData(rawData, 1, ConvertRawDataToInt8, Size);
}
else
{
numElements /= 2;
size = (int)Mathf.Sqrt(numElements);
size2 = size * size;
// Is this a 16-bit raw image?
if (size2 == numElements)
{
Size = new Vector2Int(size, size);
m_NormalisedHeights = CalculateHeightData(rawData, 2, ConvertRawDataToInt16, Size);
}
else
{
throw new InvalidOperationException("Cannot generate a height-map from non-square data.");
}
}
FlipHeightsInPlace(flip);
}
public Heightmap(Heightmap parentHeightmap, Vector2Int offset, Vector2Int size, float remap, float baseLevel)
{
// Initialise our dimensions and allocate the necessary memory...
Size = size;
Remap = remap;
Base = baseLevel;
m_NormalisedHeights = GenerateBlendedHeightDataFromParent(parentHeightmap, offset, size);
}
public Heightmap(float[,] heights, Flip flip)
{
int numRows = heights.GetLength(0);
int numColumns = heights.GetLength(1);
Size = new Vector2Int(numRows, numColumns);
m_NormalisedHeights = heights;
FlipHeightsInPlace(flip);
}
public Heightmap(Vector2Int size)
{
Size = size;
m_NormalisedHeights = new float[Height, Width];
}
///
/// Sets the array of normalised heights at the position specified in the height-map.
///
///
///
public void SetNormalisedHeights(Vector2Int offset, float[,] normalisedHeights)
{
int specifiedWidth = normalisedHeights.GetLength(1);
int specifiedHeight = normalisedHeights.GetLength(0);
for (int x = 0; x < specifiedWidth; x++)
{
for (int y = 0; y < specifiedHeight; y++)
{
float normalisedHeight = normalisedHeights[y, x];
m_NormalisedHeights[offset.y + y, offset.x + x] = normalisedHeight;
}
}
}
///
/// Gets the normalised height at the specified location in the height-map.
///
/// The co-ordinates of the height to fetch.
/// The normalised height at the location specified.
public float GetNormalisedHeightAt(Vector2 offset)
{
int x = (offset.x >= Width) ? Width - 1 : (int)offset.x;
int y = (offset.y >= Height) ? Height - 1 : (int)offset.y;
float height = m_NormalisedHeights[y, x];
return height;
}
///
/// Apply the data held by this height-map to the specified piece of terrain.
///
/// The terrain to apply the height-map to.
/// The size of the terrain to be generated,
public void ApplyTo(Terrain terrain)
{
terrain.terrainData.heightmapResolution = Width;
terrain.terrainData.SetHeights(0, 0, m_NormalisedHeights);
}
///
/// Generate a Texture2D from the current height-map applying a checkerboard effect on the alpha-channel.
///
/// The size of the texture to generate.
/// The number of tiles on the checkerboard.
/// The alpha of the dark checkerboard tiles.
/// The Texture2D generated.
public Texture2D ToTexture2D(Vector2Int textureSize, Vector2Int checkerboardDimensions, float checkerboardAlpha)
{
const TextureFormat format = TextureFormat.ARGB32;
const bool mipChain = false;
const bool linear = true;
Vector2Int checkerboardSize = new Vector2Int(textureSize.x / checkerboardDimensions.x, textureSize.y / checkerboardDimensions.y);
Texture2D newTexture = new Texture2D(textureSize.x, textureSize.y, format, mipChain, linear);
float incrementX = (float)Width / textureSize.x;
float incrementY = (float)Height / textureSize.y;
float heightmapY = 0.0f;
// No filtering here - simply read a value for each pixel in the output texture...
for (int textureY = 0; textureY < textureSize.y; textureY++, heightmapY += incrementY)
{
int checkerboardY = textureY / checkerboardSize.y;
float heightmapX = 0.0f;
for (int textureX = 0; textureX < textureSize.x; textureX++, heightmapX += incrementX)
{
int checkerboardX = textureX / checkerboardSize.x;
int checkerboardIndex = checkerboardX + checkerboardY;
float normalisedHeight = m_NormalisedHeights[(int)heightmapY, (int)heightmapX];
float alpha = ((checkerboardIndex & 1) == 0) ? 1.0f : checkerboardAlpha;
Color color = new Color(normalisedHeight, normalisedHeight, normalisedHeight, alpha);
newTexture.SetPixel(textureX, textureY, color);
}
}
newTexture.Apply();
return newTexture;
}
///
/// Generate a Texture2D from the current height-map applying a checkerboard effect on the alpha-channel.
///
/// The number of tiles on the checkerboard.
/// The alpha of the dark checkerboard tiles.
/// The Texture2D generated.
public Texture2D ToTexture2D(Vector2Int checkerboardDimensions, float checkerboardAlpha)
{
Vector2Int textureSize = new Vector2Int(Width, Height);
return ToTexture2D(textureSize, checkerboardDimensions, checkerboardAlpha);
}
///
/// Generate a Texture2D from the current height-map.
///
/// The Texture2D generated.
public Texture2D ToTexture2D()
{
Vector2Int oneByOne = new Vector2Int(1, 1);
return ToTexture2D(oneByOne, 0.5f);
}
}
}