// Procedural Terrain Painter by Staggart Creations http://staggart.xyz // Copyright protected under Unity Asset Store EULA using System.Collections.Generic; using UnityEngine; #if UNITY_2021_2_OR_NEWER using UnityEngine.TerrainTools; #else using UnityEngine.Experimental.TerrainAPI; #endif namespace sc.terrain.proceduralpainter { public class ModifierStack { private static int m_resolution; private static float heightScale; public static Material filterMat; private static RenderTexture alphaMap; private const string UndoActionName = "Painted Terrain"; private static int HeightmapID = Shader.PropertyToID("_Heightmap"); private static int HeightmapScaleID = Shader.PropertyToID("_HeightmapScale"); private static int NormalMapID = Shader.PropertyToID("_NormalMap"); private static int TerrainPosScaleID = Shader.PropertyToID("_TerrainPosScale"); private static int TerrainBoundsID = Shader.PropertyToID("_TerrainBounds"); /// /// Call this once, per terrain. Sets up the Blit material and generates a normal map /// /// /// /// public static void Configure(Terrain terrain, Bounds bounds, int resolution) { if (m_resolution != resolution || alphaMap == null) { alphaMap = new RenderTexture(resolution, resolution, 0, RenderTextureFormat.R8); } m_resolution = resolution; if (!filterMat) filterMat = new Material(Shader.Find("Hidden/TerrainPainter/Modifier")); //Note: heightmap borders aren't blended with neighboring texels, causes visible seams at low resolutions filterMat.SetTexture(HeightmapID, terrain.terrainData.heightmapTexture); filterMat.SetTexture(NormalMapID, terrain.normalmapTexture); heightScale = bounds.max.y - bounds.min.y; filterMat.SetFloat(HeightmapScaleID, heightScale); //Used to reconstruct the global-bounds aligned UV float invWidth = 1.0f / bounds.size.x; float invHeight = 1.0f / bounds.size.z; var terrainPosScale = new Vector4( (terrain.GetPosition().x * invWidth) - (bounds.min.x * invWidth), (terrain.GetPosition().z * invHeight) - (bounds.min.z * invHeight), terrain.terrainData.size.x / bounds.size.x, terrain.terrainData.size.z / bounds.size.z ); filterMat.SetVector(TerrainPosScaleID, terrainPosScale); filterMat.SetVector(TerrainBoundsID, new Vector4(bounds.min.x, bounds.max.z, bounds.size.x, bounds.size.z)); } public static void ProcessLayers(Terrain terrain, List layerSettings) { for (int i = layerSettings.Count-1; i >= 0; i--) { ProcessSingleLayer(terrain, layerSettings[i]); } } /// /// Executes the modifiers in order and generates the necessary splatmaps /// /// /// /// public static void ProcessSingleLayer(Terrain terrain, LayerSettings settings) { //Disable or with no settings (result in a fill with black) if (settings.enabled == false || settings.layer == false) return; Graphics.SetRenderTarget(alphaMap); //Start with a full white base Graphics.Blit(Texture2D.whiteTexture, alphaMap); //Reverse order for (int i = settings.modifierStack.Count-1; i >= 0; i--) { settings.modifierStack[i].Configure(filterMat); settings.modifierStack[i].Execute(alphaMap); } //Scale splatmap to fit in terrain Vector2 scaledSplatmapSize = new Vector2((terrain.terrainData.size.x / m_resolution) * m_resolution, (terrain.terrainData.size.z / m_resolution) * m_resolution); //PaintContext handles creation of splatmaps. Subtracting the weights of a splatmap, from the ones before it. //A single pixels of all combined alpha maps must not exceed a value of one. The 2nd pass of Hidden/TerrainEngine/TerrainLayerUtils is used internally to do this //Note: Paintcontext must use a serialized terrain reference, otherwise breaks when executing "ApplyDelayedActions" when the project is saved! PaintContext c = TerrainPaintUtility.BeginPaintTexture(terrain, new Rect(0, 0, scaledSplatmapSize.x, scaledSplatmapSize.y), settings.layer); Graphics.Blit(alphaMap, c.destinationRenderTexture); TerrainPaintUtility.EndPaintTexture(c, UndoActionName); } } }