using System; using System.Collections.Generic; using System.ComponentModel; using System.Security.Permissions; using UnityEditor; using UnityEngine; namespace Framework.Terrain { /// Helper methods for dealing with terrains public class TerrainHelper { #region struct HeightRange /// Container that stores a minimum and a maximum height private struct HeightRange { /// Default range initialized to 0 public static readonly HeightRange Default = new HeightRange(0.0f, 0.0f); /// Initializes a new height range /// Minimum height that will be assigned to the height range /// Maximum height that will be assigned to the height range public HeightRange(float min, float max) { this.Min = min; this.Max = max; } /// Lower height in the height range public float Min; /// Upper height in the height range public float Max; } #endregion // struct HeightRange #region class LayerWeightCalculator /// Calculates the layer weights when imported from texture colors private class LayerWeightCalculator { /// Maximum number of layers this helper class can deal with const int MaximumLayerCount = 8; /// Initializes a new layer weight calculator /// Textures the layer weights will be imported from /// Assignments of color channels to splat layers public LayerWeightCalculator(Texture2D[] textures, int[] colorIndices) { if(textures != null) { if(textures.Length >= 2) { this.firstTexture = textures[0]; this.secondTexture = textures[1]; } else if (textures.Length >= 1) { this.firstTexture = textures[0]; } } this.colorIndices = colorIndices; if(colorIndices != null) { this.colorIndexCount = colorIndices.Length; } this.weights = new float[MaximumLayerCount]; } /// Calculates the normalized layer weights from the colors /// X coordinate on the splat weight textures /// Y coordinate on the splat weight textures /// An array containing the normalized layer weights public float[] CalculateWeights(float u, float v) { clearWeightBuffer(); // Add all weights involving the first texture if a first texture is present if(this.firstTexture != null) { Color firstColor = this.firstTexture.GetPixelBilinear(u, v); for(int index = 0; index < this.colorIndexCount; ++index) { switch(this.colorIndices[index]) { case 1: { this.weights[index] = firstColor.r; break; } case 2: { this.weights[index] = firstColor.g; break; } case 3: { this.weights[index] = firstColor.b; break; } case 4: { this.weights[index] = firstColor.a; break; } } } } // Add all weights involving the second texture if a second texture is present if(this.secondTexture != null) { Color secondColor = this.secondTexture.GetPixelBilinear(u, v); for(int index = 0; index < this.colorIndexCount; ++index) { switch(this.colorIndices[index]) { case 5: { this.weights[index] = secondColor.r; break; } case 6: { this.weights[index] = secondColor.g; break; } case 7: { this.weights[index] = secondColor.b; break; } case 8: { this.weights[index] = secondColor.a; break; } } } } // Calculate the sum of all weights float sum = 0.0f; for(int index = 0; index < MaximumLayerCount; ++index) { sum += this.weights[index]; } // Unless there are no weights (Div-by-0 danger), normalize all weights if(sum > 0.0f) { for(int index = 0; index < MaximumLayerCount; ++index) { this.weights[index] /= sum; } } return this.weights; } /// Resets all weights to zero private void clearWeightBuffer() { //Array.Clear(this.weights, 0, MaximumLayerCount); for(int index = 0; index < MaximumLayerCount; ++index) { this.weights[index] = 0.0f; } } /// First texture from which to take color values private Texture2D firstTexture; /// Second texture from which to take color values private Texture2D secondTexture; /// Which color channels are mapped to which splat layers private int[] colorIndices; /// Number of color indices present private int colorIndexCount; /// Temporary buffer to hold retrieved weights for normalization /// /// Reusing this buffer avoids feeding the garbage collector each pixel /// private float[] weights; } #endregion // class LayerWeightCalculator /// Imports terrain heights from a texture /// Terrain data the heights will be assigned to /// Texture the heights will be taken from public static void ImportHeightsFromTexture( TerrainData terrainData, Texture2D texture ) { int terrainWidthAndHeight = terrainData.heightmapResolution; float[,] heights = terrainData.GetHeights( 0, 0, terrainWidthAndHeight, terrainWidthAndHeight ); for(int y = 0; y < terrainWidthAndHeight; ++y) { float v = (float)(y + 0.5f) / (float)(terrainWidthAndHeight); for(int x = 0; x < terrainWidthAndHeight; ++x) { float u = (float)(x + 0.5f) / (float)(terrainWidthAndHeight); Color color = texture.GetPixelBilinear(u, v); heights[y, x] = color.grayscale; } } terrainData.SetHeights(0, 0, heights); } /// Returns the terrain heights as a texture /// Terrain data the heights will be taken from /// A texture containing the terrain heights /// Heights are normalized across the actually used height range public static Texture2D GetHeightsAsTexture(TerrainData terrainData) { int terrainWidthAndHeight = terrainData.heightmapResolution; var texture = new Texture2D( terrainWidthAndHeight, terrainWidthAndHeight, TextureFormat.RGBA32, false, true ); // Take the pixel color values from Unity... float[,] heights = terrainData.GetHeights( 0, 0, terrainWidthAndHeight, terrainWidthAndHeight ); HeightRange heightExtremes = getHeightExtremes(heights); heightExtremes.Max -= heightExtremes.Min; // ...and store them in our texture as gray values var line = new Color32[terrainWidthAndHeight]; for(int v = 0; v < terrainWidthAndHeight; ++v) { for(int u = 0; u < terrainWidthAndHeight; ++u) { float height = (heights[v, u] - heightExtremes.Min) * 255.0f / heightExtremes.Max; // We could just set all 3 channels to the closest brightness, // but we can do better: if we choose the r,g,b channels with the assumption // that their average becomes the grey value, we have 768 gray levels // (all without impacting compatibility). // // 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 // // red 0 0 0 0 0 | 1 1 1 1 1 // green 0 0 0 0 0 0 0 | 1 1 1 1 // blue 0 0 0 0 | 1 1 1 1 1 1 1 // // 0.2: 254+254+254 = 254.0 - 0+0+0 = 0.0 // 0.4: 254+254+255 = 254.33 - 0+0+1 = 0.33 // 0.6: 255+254+255 = 254.67 - 1+0+1 = 0.67 // 0.8: 255+255+255 = 255.0 - 1+1+1 = 1.0 // line[u].r = (byte)(height + 0.5f); line[u].g = (byte)(height + (1.0f / 3.0f)); line[u].b = (byte)(height + (2.0f / 3.0f)); line[u].a = 255; } texture.SetPixels32(0, v, terrainWidthAndHeight, 1, line); } return texture; } /// Imports the splat layer weights from one or more textures /// Terrain data to which the layer weights will be applied /// Textures the layer weights will be taken from /// Colors to map to each of the texture layers public static void ImportLayerWeightsFromTextures( TerrainData terrainData, Texture2D[] textures, int[] colorIndices ) { int terrainWidthAndHeight = terrainData.alphamapResolution; int layerCount = colorIndices.Length; if(layerCount > terrainData.alphamapLayers) { throw new IndexOutOfRangeException( "Layer count is higher than the number of layers present in terrain" ); } float[,,] layerWeights = terrainData.GetAlphamaps( 0, 0, terrainWidthAndHeight, terrainWidthAndHeight ); var layerWeightCalculator = new LayerWeightCalculator(textures, colorIndices); for(int y = 0; y < terrainWidthAndHeight; ++y) { float v = (float)(y + 0.5f) / (float)(terrainWidthAndHeight); for(int x = 0; x < terrainWidthAndHeight; ++x) { float u = (float)(x + 0.5f) / (float)(terrainWidthAndHeight); float[] weights = layerWeightCalculator.CalculateWeights(u, v); for(int index = 0; index < layerCount; ++index) { layerWeights[y, x, index] = weights[index]; } } } terrainData.SetAlphamaps(0, 0, layerWeights); } /// Returns the terrain layer weights as a texture /// Terrain data the layer weights will be taken from /// Layer indices that will be stored in the color channels /// A texture containing the terrain layer weights public static Texture2D GetLayerWeightsAsTexture(TerrainData terrainData, int[] layerIndices) { int terrainWidthAndHeight = terrainData.alphamapResolution; var texture = new Texture2D( terrainWidthAndHeight, terrainWidthAndHeight, TextureFormat.RGBA32, false, true ); // Ensure no more than 4 layers have been specified int layerCount = getArrayLength(layerIndices); if(layerCount > 4) { throw new ArgumentException("Too many layers specified (maximum is 4)", "layerIndices"); } // Fail if any color channel is assigned to a layer that doesn't exist for(int index = 0; index < layerCount; ++index) { if(layerIndices[index] > terrainData.alphamapLayers) { throw new IndexOutOfRangeException( "Layer index is higher than number of layers present in terrain" ); } } // Take the layer weights from Unity... float[,,] layerWeights = terrainData.GetAlphamaps( 0, 0, terrainWidthAndHeight, terrainWidthAndHeight ); // And store them in our texture's color channels according to the layer mapping table var line = new Color32[terrainWidthAndHeight]; for(int v = 0; v < terrainWidthAndHeight; ++v) { for(int u = 0; u < terrainWidthAndHeight; ++u) { line[u] = new Color32(0, 0, 0, 255); for(int index = 0; index < layerCount; ++index) { int layerIndex = layerIndices[index]; if(layerIndex != -1) { byte colorValue = (byte) (layerWeights[v, u, layerIndex] * 255.0f + 0.5f); assignColor(ref line[u], index, colorValue); } } } texture.SetPixels32(0, v, terrainWidthAndHeight, 1, line); } return texture; } /// Determines the minimum and maximum height in a height array /// Height array that will be scanned /// The minimum and maximum heights found within the array private static HeightRange getHeightExtremes(float[,] heights) { if(heights == null) { return HeightRange.Default; } int rowCount = heights.GetLength(0); int colCount = heights.GetLength(1); if((rowCount == 0) || (colCount == 0)) { return HeightRange.Default; } var extremes = new HeightRange(heights[0, 0], heights[0, 0]); for(int v = 0; v < rowCount; ++v) { for(int u = 0; u < colCount; ++u) { float height = heights[v, u]; if(height < extremes.Min) { extremes.Min = height; } if(height > extremes.Max) { extremes.Max = height; } } } return extremes; } /// Returns the lenght of an array and 0 if the array is null /// Array whose length will be returned /// The length of the array, even if the array itself is null private static int getArrayLength(int[] array) { if(array == null) { return 0; } else { return array.Length; } } /// Assigns a color channel by index ot a 32 bit RGB color value /// Color in which a color channel will be assigned by index /// Index of the color channel that will be assigned /// Value that will be assigned to the color channel private static void assignColor(ref Color32 color, int colorChannelIndex, byte value) { switch(colorChannelIndex) { case 0: { color.r = value; break; } case 1: { color.g = value; break; } case 2: { color.b = value; break; } case 3: { color.a = value; break; } default: { throw new IndexOutOfRangeException("Color channel index out of range"); } } } } } // namespace Framework.Terrain