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); } } }