// Procedural Terrain Painter by Staggart Creations http://staggart.xyz // Copyright protected under Unity Asset Store EULA using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif #if __MICROSPLAT__ using JBooth.MicroSplat; #endif namespace sc.terrain.proceduralpainter { [ExecuteInEditMode] public partial class TerrainPainter : MonoBehaviour { public const string Version = "1.0.4"; public static TerrainPainter Current; public Terrain[] terrains; [Attributes.ResolutionDropdown(64, 1024)] public int splatmapResolution = 256; [Attributes.ResolutionDropdown(16, 2048)] [Tooltip("The color/base map is a pre-rendered texture for the terrain color. This is shown on the terrain in the distance. High resolutions usually have little benefit")] public int colorMapResolution = 256; public List layerSettings = new List(); /// /// Repaints a terrain if its heightmap is modified. The SetAutoRepaint function must be used to set this up for the terrains. In the editor, the inspector automatically handles this /// [Tooltip("Automatically repaint the terrains if their heightmap is modified. Repaints when the left-mouse button is released")] public bool autoRepaint; public List terrainListeners = new List(); public Bounds bounds; #if VEGETATION_STUDIO_PRO [Tooltip("Refreshes the vegetation systems after painting. If vegetation items use terrain layers masks, this is useful")] public bool refreshVegetationOnPaint; #endif #if __MICROSPLAT__ [Tooltip("Assign the TextureArrayConfig asset here. Adding, removing or re-ordering layers will be also be applied to the texture array")] public TextureArrayConfig msTexArray; #endif [SerializeField] //Reference it once here, so it gets included in a build private Shader filterShader; public delegate void TerrainRepaintEvent(Terrain terrain); /// /// Triggers whenever a terrain is repainted. Passes the context terrain as a parameter. /// public static event TerrainRepaintEvent OnTerrainRepaint; private void OnEnable() { if (!filterShader) filterShader = Shader.Find("Hidden/TerrainPainter/Modifier"); Current = this; } /// /// Applies the splatmapResolution value to all terrains. This must be called when changing the resolution before repainting a single terrain. Automatically done in the function /// public void ResizeSplatmaps() { //Needs to happen before repainting, all terrains must have the same splatmap resolution. PaintContext throws warnings otherwise foreach (Terrain terrain in terrains) { if(terrain) terrain.terrainData.alphamapResolution = splatmapResolution; } } public void RecalculateBounds() { bounds = Utilities.RecalculateBounds(terrains); } [ContextMenu("Assign active terrains")] public void AssignActiveTerrains() { SetTargetTerrains(Terrain.activeTerrains); RepaintAll(); } public void SetTargetTerrains(Terrain[] terrains) { this.terrains = terrains; RecalculateBounds(); } /// /// Repaints all the assigned terrains using the current configuration /// public void RepaintAll() { if (layerSettings.Count == 0) return; ResizeSplatmaps(); foreach (Terrain terrain in terrains) { if (!terrain) { Debug.LogError("Missing terrain assigned to TerrainPainter", this); continue; } RepaintTerrain(terrain); } //ApplyAllStamps(); } /// /// Repaints an individual terrain /// /// public void RepaintTerrain(Terrain terrain) { if (layerSettings.Count == 0 || terrain == null) return; ModifierStack.Configure(terrain, bounds, splatmapResolution); ModifierStack.ProcessLayers(terrain, layerSettings); ApplyStampsToTerrain(terrain); //Regenerate basemap terrain.terrainData.baseMapResolution = colorMapResolution; terrain.terrainData.SetBaseMapDirty(); RefreshVegetation(terrain); terrain.Flush(); OnTerrainRepaint?.Invoke(terrain); #if UNITY_EDITOR EditorUtility.SetDirty(terrain.terrainData); #endif } private void RefreshVegetation(Terrain terrain) { #if VEGETATION_STUDIO_PRO AwesomeTechnologies.VegetationStudio.VegetationStudioManager manager = AwesomeTechnologies.VegetationStudio.VegetationStudioManager.Instance; if (refreshVegetationOnPaint && manager) { #if UNITY_2019_1_OR_NEWER //VS requires the CPU data terrain.terrainData.SyncTexture(TerrainData.AlphamapTextureName); #endif foreach (AwesomeTechnologies.VegetationSystem.VegetationSystemPro sys in manager.VegetationSystemList) { if (sys == null) continue; sys.ClearCache(); sys.RefreshTerrainArea(terrain.terrainData.bounds); } #if UNITY_EDITOR SceneView.RepaintAll(); #endif } #endif } public void CreateSettingsForLayer(TerrainLayer layer) { LayerSettings s = new LayerSettings(); s.layer = layer; s.modifierStack = new List(); layerSettings.Insert(0, s); SetTerrainLayers(); } /// /// Adds or removes the TerrainChangeListener component from all assigned terrains. If enabled, terrains will be repainted when their height is modified /// /// public void SetAutoRepaint(bool value) { autoRepaint = value; if (value) { RemoveTerrainListeners(); foreach (Terrain terrain in terrains) { TerrainChangeListener listener = terrain.GetComponent(); if (!listener) listener = terrain.gameObject.AddComponent(); listener.terrain = terrain; this.terrainListeners.Add(listener); } } else { RemoveTerrainListeners(); } } private void RemoveTerrainListeners() { for (int i = 0; i < terrainListeners.Count; i++) { DestroyImmediate(terrainListeners[i]); } terrainListeners.Clear(); } /// /// Ensures that all configured layers are in fact assigned to the terrains. Also removed if they were. /// [ContextMenu("Set terrain layers")] public void SetTerrainLayers() { TerrainLayer[] layers = Utilities.SettingsToLayers(layerSettings); foreach (Terrain terrain in terrains) { terrain.terrainData.terrainLayers = layers; terrain.terrainData.SetBaseMapDirty(); #if UNITY_EDITOR EditorUtility.SetDirty(terrain.terrainData); #endif } } #region Virtual //Only should be called after the modifiers have been applied, otherwise it acts as a persistent brush private void ApplyStampsToTerrain(Terrain terrain) { ApplyStamps(terrain); } partial void ApplyAllStamps(); partial void ApplyStamps(Terrain terrain); #endregion } }