using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; namespace Framework.Terrain { /// Imports terrain from various sources [CustomEditor(typeof(TerrainImportSettings))] public class TerrainImportEditor : Editor { /// Available color channels in a standard 32 bit texture private readonly static string[] ColorChannels = new string[] { "None", "R", "G", "B", "A", "R2", "G2", "B2", "A2" }; /// Actions the terrain importer/exporter can perform private enum ImportExportAction { /// Do nothing None, /// Import terrain heights from height map ImportHeightMap, /// Export terrain heights to height map ExportHeightMap, /// Import splat maps as terrain layer weights ImportSplatMaps, /// Export up to four terrain layer weights as a splat map ExportPrimarySplatMap, /// Export up to four additional terrain layer weights as a splat map ExportSecondarySplatMap }; /// Draws the terrain import tool panel in the Inspector public override void OnInspectorGUI() { var terrainSettings = base.target as TerrainImportSettings; if(terrainSettings == null) { GUILayout.Label( "Terrain import settings not displayed because " + "TerrainImportSettings component not present" ); return; } UnityEngine.Terrain terrain = terrainSettings.GetComponent(); if(terrain == null) { GUILayout.Label( "Terrain import settings not displayed because Terrain component is missing" ); return; } // Show the UI for setting up height map import/export doHeightMapGUI(terrainSettings); ImportExportAction heightMapAction = doHeightMapButtonGUI(); displayWarningsForMissingHeightTexture(terrainSettings); EditorGUILayout.Separator(); // Show the UI for settings up splat map import/export doSplatLayerGUI(terrain, terrainSettings); ImportExportAction splatMapAction = doSplatMapButtonGUI(); displayWarningsForMissingSplatTextures(terrainSettings); // If the user clicked one of the buttons, do the appropriate action if(heightMapAction == ImportExportAction.ImportHeightMap) { importHeightMapFromTexture(terrainSettings, terrain.terrainData); } else if(heightMapAction == ImportExportAction.ExportHeightMap) { exportHeightMapToTexture(terrain.terrainData); } else if(splatMapAction == ImportExportAction.ImportSplatMaps) { importSplatMapsFromTexture(terrainSettings, terrain.terrainData); } else if(splatMapAction == ImportExportAction.ExportPrimarySplatMap) { exportPrimarySplatMapToTexture(terrain.terrainData, terrainSettings.SplatColorIndices); } else if(splatMapAction == ImportExportAction.ExportSecondarySplatMap) { exportSecondarySplatMapToTexture(terrain.terrainData, terrainSettings.SplatColorIndices); } } /// Displays the inspector UI for the height map import settings /// Terrain settings that will be modified private void doHeightMapGUI(TerrainImportSettings terrainSettings) { const bool AllowSceneObjects = false; using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel("Height Map Input"); terrainSettings.HeightMapTexture = (Texture2D) EditorGUILayout.ObjectField( terrainSettings.HeightMapTexture, typeof(Texture2D), AllowSceneObjects ); } using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel("Minimum Height"); terrainSettings.MinHeight = EditorGUILayout.FloatField(terrainSettings.MinHeight); } using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel("Maximum Height"); terrainSettings.MaxHeight = EditorGUILayout.FloatField(terrainSettings.MaxHeight); } } /// Handles the buttons by which the height map can be imported / exported /// The action the user wishes to perform, if any private ImportExportAction doHeightMapButtonGUI() { var result = ImportExportAction.None; using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel(" "); using(var verticalScope = new EditorGUILayout.VerticalScope()) { if(GUILayout.Button("Import Height Map")) { result = ImportExportAction.ImportHeightMap; } if(GUILayout.Button("Export Height Map")) { result = ImportExportAction.ExportHeightMap; } } } return result; } /// Displays warnings if the height texture is missing /// Terrain import settings that will be checked private void displayWarningsForMissingHeightTexture(TerrainImportSettings terrainSettings) { if(terrainSettings.HeightMapTexture == null) { EditorGUILayout.HelpBox("WARNING: No height map texture assigned", MessageType.Warning); } } /// Displays the UI for setting up splat layer assignments /// Terrain from which to take the splat layers /// Terrain settings that will be modified private void doSplatLayerGUI( UnityEngine.Terrain terrain, TerrainImportSettings terrainSettings ) { const bool AllowSceneObjects = false; using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel("Splat Map Inputs"); terrainSettings.SplatMapTexture = (Texture2D) EditorGUILayout.ObjectField( terrainSettings.SplatMapTexture, typeof(Texture2D), AllowSceneObjects ); terrainSettings.SecondarySplatMapTexture = (Texture2D) EditorGUILayout.ObjectField( terrainSettings.SecondarySplatMapTexture, typeof(Texture2D), AllowSceneObjects ); } EditorGUILayout.Separator(); // If there are no layers, inform the user why he can't set up layer assignments int layerCount = terrain.terrainData.alphamapLayers; if(layerCount == 0) { GUILayout.Label("Add texture layers to your terrain to display layer assignments"); return; } // Make sure there are layers set up for each splat layer terrainSettings.ResizeSplatColorIndices(layerCount); // Let the user choose which color channel to use for each splat layer for(int index = 0; index < layerCount; ++index) { using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel("Layer " + (index + 1)); terrainSettings.SplatColorIndices[index] = GUILayout.SelectionGrid( terrainSettings.SplatColorIndices[index], ColorChannels, 9 ); } } } /// Draws the splat map buttons in the inspector /// The action the user wishes to perform, if any private ImportExportAction doSplatMapButtonGUI() { var result = ImportExportAction.None; using(var horizontalScope = new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel(" "); using(var verticalScope = new EditorGUILayout.VerticalScope()) { if(GUILayout.Button("Import Splat Map(s)")) { result = ImportExportAction.ImportSplatMaps; } using(var innerHorizontalScope = new EditorGUILayout.HorizontalScope()) { if(GUILayout.Button("Export Splat Map 1")) { result = ImportExportAction.ExportPrimarySplatMap; } if(GUILayout.Button("Export Splat Map 2")) { result = ImportExportAction.ExportSecondarySplatMap; } } } } return result; } /// Displays warnings if required splat textures are missing /// Terrain import settings that will be checked private void displayWarningsForMissingSplatTextures(TerrainImportSettings terrainSettings) { // Generate a string containing all texture warning string warnings = string.Empty; { bool missingPrimarySplatMapTexture = (terrainSettings.SplatMapTexture == null) && (terrainSettings.ReferencesPrimarySplatMapTexture); if(missingPrimarySplatMapTexture) { warnings = "WARNING: no primary splat map texture assigned"; } bool missingSecondarySplatMapTexture = (terrainSettings.SecondarySplatMapTexture == null) && (terrainSettings.ReferencesSecondarySplatMapTexture); if(missingSecondarySplatMapTexture) { if(missingPrimarySplatMapTexture) { warnings += "\n"; } warnings += "WARNING: no secondary splat map texture assigned"; } } // If any warning messages were triggered, display them if(!string.IsNullOrEmpty(warnings)) { EditorGUILayout.HelpBox(warnings, MessageType.Warning); } } /// Returns an array containing all currently selected terrains /// An array of all selected terrains or null if none were selected private UnityEngine.Terrain[] getSelectedTerrains() { GameObject[] selectedGameObjects = UnityEditor.Selection.gameObjects; if(selectedGameObjects == null) { return null; } int selectedGameObjectCount = selectedGameObjects.Length; var terrains = new List(capacity: selectedGameObjectCount); for(int index = 0; index < selectedGameObjectCount; ++index) { UnityEngine.Terrain terrain = selectedGameObjects[index].GetComponent(); if(terrain != null) { terrains.Add(terrain); } } if(terrains.Count == 0) { return null; } else { return terrains.ToArray(); } } /// Imports the terrain's heights from a height map texture /// Import settings containing the texture /// Terrain the height map will be imported into private void importHeightMapFromTexture( TerrainImportSettings importSettings, TerrainData terrainData ) { if(importSettings.HeightMapTexture == null) { EditorUtility.DisplayDialog( "Texture not Assigned", "There is no height map texture assigned to the import from", "OK" ); return; } TerrainHelper.ImportHeightsFromTexture(terrainData, importSettings.HeightMapTexture); } /// Exports the terrain to a height map texture /// Terrain data that will be exported private void exportHeightMapToTexture(TerrainData terrainData) { Texture2D heightMapTexture = TerrainHelper.GetHeightsAsTexture(terrainData); string path = EditorUtility.SaveFilePanel( "Select Location to Save Height Map", ".", "heightmap.png", "png" ); if(!string.IsNullOrEmpty(path)) { writeToFile(path, heightMapTexture.EncodeToPNG()); } } /// Imports the terrain's splat maps from one or two textures /// Import settings containing the textures /// Terrain the splat layer weights will be applied to private void importSplatMapsFromTexture( TerrainImportSettings importSettings, TerrainData terrainData ) { var textures = new Texture2D[] { importSettings.SplatMapTexture, importSettings.SecondarySplatMapTexture }; TerrainHelper.ImportLayerWeightsFromTextures( terrainData, textures, importSettings.SplatColorIndices ); } /// Saves the terrain's primary splat map into a file /// Terrain whose primary splat map will be saved /// Color channel assignments for the splat layers private void exportPrimarySplatMapToTexture( TerrainData terrainData, int[] splatColorIndices ) { int[] splatLayerIndices = layerIndicesFromColorIndices(splatColorIndices, -1); Texture2D splatMapTexture = TerrainHelper.GetLayerWeightsAsTexture( terrainData, splatLayerIndices ); string path = EditorUtility.SaveFilePanel( "Select Location to Save Primary Splat Map", ".", "splatmap1.png", "png" ); if(!string.IsNullOrEmpty(path)) { writeToFile(path, splatMapTexture.EncodeToPNG()); } } /// Saves the terrain's secondary splat map into a file /// Terrain whose secondary splat map will be saved /// Color channel assignments for the splat layers private void exportSecondarySplatMapToTexture( TerrainData terrainData, int[] splatColorIndices ) { int[] splatLayerIndices = layerIndicesFromColorIndices(splatColorIndices, -5); Texture2D splatMapTexture = TerrainHelper.GetLayerWeightsAsTexture( terrainData, splatLayerIndices ); string path = EditorUtility.SaveFilePanel( "Select Location to Save Secondary Splat Map", ".", "splatmap2.png", "png" ); if(!string.IsNullOrEmpty(path)) { writeToFile(path, splatMapTexture.EncodeToPNG()); } } /// Writes the contents of a byte array into a file /// Path the byte array's contents will be saved under /// File contents that will be saved as a file private static void writeToFile(string path, byte[] fileContents) { using( var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None) ) { stream.Write(fileContents, 0, fileContents.Length); stream.Flush(); } } /// /// Converts a set of color indices per layer to a set of layer indices per color /// (transposes a look up table) /// /// Color indices that will be converted to layer indices /// Offset by which the colors will be shifted /// Layer indices for each color private static int[] layerIndicesFromColorIndices(int[] colorIndices, int colorOffset = 0) { const int MaximumColorChannelCount = 4; if(colorIndices == null) { return null; } int colorIndexCount = colorIndices.Length; // Initialize the layer indices to -1 so unused layers will be clearly marked var layerIndices = new int[MaximumColorChannelCount]; for(int index = 0; index < MaximumColorChannelCount; ++index) { layerIndices[index] = -1; } // Walk through all color channel assignments and fill the layer indices // at each color channel with the layer index it was found on for(int index = 0; index < colorIndexCount; ++index) { int colorChannelIndex = colorIndices[index] + colorOffset; if((colorChannelIndex >= 0) && (colorChannelIndex < MaximumColorChannelCount)) { layerIndices[colorChannelIndex] = index; } } return layerIndices; } } } // namespace Framework.Terrain