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