using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Experimental.Rendering; using Object = UnityEngine.Object; namespace UnityEditor.TerrainTools { /// /// Functions for manipulating terrain details /// public static class DetailUtility { /// /// Gets a TerrainData's detailLayer as a Texture2D /// /// a TerrainData object /// the detail layer to use /// Texture2D representing the grayscale values of the detail masks public static Texture2D GetDensityMapTexture(TerrainData tData, int detailLayer = 0) { var detailWidth = tData.detailWidth; var detailHeight = tData.detailHeight; var bucketSize = InstanceCountMultiplier(tData); var detailArray = tData .GetDetailLayer(0, 0, detailWidth, detailHeight, detailLayer) .Cast() .Select(i => (byte) Mathf.Min(i * bucketSize, 255)); var outputTexture = new Texture2D(detailWidth, detailHeight, TextureFormat.R8, false); outputTexture.name = $"{tData.name}_densitymap"; outputTexture.SetPixelData(detailArray.ToArray(), 0); outputTexture.Apply(false); return outputTexture; } /// /// Saves a Texture2D representing the detail density in /// to the asset folder /// /// A terrain object to save /// An asset folder to receive saved assets /// The detail layer to save public static void SaveDensityMap(Terrain terrain, DefaultAsset folder, int detailLayer) { var texToSave = GetDensityMapTexture(terrain.terrainData, detailLayer); var outputFile = GetDensityMapName(terrain, folder, detailLayer); AssetDatabase.CreateAsset(texToSave, outputFile); Debug.Log($"Saved {outputFile}"); } /// /// Save a detail density map for every prototype in the supplied terrain to /// /// /// public static void SaveAllDensityMaps(Terrain terrain, DefaultAsset folder) { var layers = terrain.terrainData.detailPrototypes.Length; for (var i = 0; i < layers; i++) SaveDensityMap(terrain, folder, i); } /// /// Save a detail density map for every prototype in the supplied terrain to /// /// /// String path to the asset folder public static void SaveAllDensityMaps(Terrain terrain, string folderName) { var folder = AssetDatabase.LoadAssetAtPath(folderName); if (folder is null) throw new ArgumentException($"folderAsset {folderName} not found"); SaveAllDensityMaps(terrain, folder); } /// /// Generates a predictable asset file path for a texture assert representing a detail density map /// /// The terrain object whose details will be saved /// A DefaultAsset representing the Asset folder where the files will be saved /// The index of the detail layer being saveds /// private static string GetDensityMapName(Terrain terrain, DefaultAsset folderAsset, int detailLayer) { return $"{AssetDatabase.GetAssetPath(folderAsset)}/{terrain.name}_details_{detailLayer:00}.asset"; } /// /// Loads a saved density map for from /// into detail layer /// /// Terrain object /// DefaultAsset or string folder path /// integer detail layer /// True if the expected texture asset exists and can be applied, or false otherwise. public static bool LoadDensityMap(Terrain terrain, DefaultAsset folder, int detailLayer) { if (detailLayer > terrain.terrainData.detailPrototypes.Length) { Debug.Log($"{terrain.name} has no detail layer {detailLayer}"); return false; } var inputFile = GetDensityMapName(terrain, folder, detailLayer); var inputTx = AssetDatabase.LoadAssetAtPath(inputFile); if (inputTx is null) { var expected = GetDensityMapName(terrain, folder, detailLayer); Debug.Log($"Could not find map {expected} for detail layer {detailLayer}"); return false; } SetDensityMap(terrain.terrainData, inputTx, detailLayer); Debug.Log($"Applied {inputFile} to {terrain.name} detail layer {detailLayer}"); return true; } /// /// Loads a saved density map for from /// into detail layer /// /// Terrain object /// string folder path /// integer detail layer /// True if the expected texture asset exists and can be applied, or false otherwise. public static bool LoadDensityMap(Terrain terrain, string folderName, int detailLayer) { var folder = AssetDatabase.LoadAssetAtPath(folderName); if (folder is null) throw new ArgumentException($"folderAsset {folderName} not found"); return LoadDensityMap(terrain, folder, detailLayer); } /// /// Given a DefaultAsset to seach, finds the density maps corresponding to /// and loads them into the corresponding detail channels of . /// /// a Terrain object /// a DefaultAsset corresponding to Asset folder containing textures public static void LoadDensityMaps(Terrain terrain, DefaultAsset folder) { Undo.RegisterCompleteObjectUndo(terrain.terrainData, "Load saved density maps"); var tdata = terrain.terrainData; var layers = tdata.detailPrototypes.Length; var restored = 0; for (var i = 0; i < layers; i++) { var loaded = LoadDensityMap(terrain, folder, i); if (loaded) restored++; i++; } if (restored > 0) Debug.Log($"Loaded {restored} detail maps"); else Debug.LogWarning("Unable to find matching detail maps"); } /// /// Given a the text path of an asset folder to seach, finds the detail maps corresponding /// to and loads them into the corresponding detail channels of . /// /// a Terrain object /// an Asset folder containing textures public static void LoadDensityMaps(Terrain terrain, string folderName) { var folder = AssetDatabase.LoadAssetAtPath(folderName); if (folder is null) throw new ArgumentException($"folderAsset {folderName} not found"); LoadDensityMaps(terrain, folder); } /// /// Populates a TerrainData's detail layer using the red channel of the supplied texture as a density /// map. Bright texels translate to more detail instances, dark texels translate to /// fewer or none. /// By default, the texture will be resampled to the correct size for the TerrainData's detail /// array. If the optional allowResample parameter is passed as false, the method will /// throw an ArgumentException if the texture and detail resolutions don't match. /// This method will accept any Texture2D as an input, but for best results use a texture /// which is not saved with gamma or color correction. /// /// the TerrainData to populate /// a Texture2D. /// the detail layer to populate /// /// if true, resample the texture to fit the detail array. If false, accept only textures of /// the same dimensions as the detail array /// /// /// thrown if allowResample is false and the texture resolution does not match the /// detail array resolution /// public static void SetDensityMap(TerrainData tData, in Texture2D texture, int detailLayer = 0, bool allowResample = true) { var w = tData.detailWidth; var h = tData.detailHeight; var assignableTexture = texture; if (w != texture.width || h != texture.height) { // if allowResample is false, we except on mismatched textures sizes to prevent an unintended // RenderTexture copy otherwise, we use Graphics.Blit to resample the texture to size if (!allowResample) { var msg = $"SetDensityMap expected a texture of {w}X{h} pixels, got {texture.width}X{texture.height} pixels"; throw new ArgumentException(msg); } using (var targetRT = new RenderTextureContext(w, h)) { Graphics.Blit(texture, targetRT); assignableTexture = DensityMapFromRenderTexture(targetRT); } } if (assignableTexture.isReadable == false) throw new ArgumentException( $"Texture {texture} is not readable. Set it readable in the import settings, or use a dynamically created texture"); var grayscale = GrayscaleMultiplier(tData); var grayValues = assignableTexture.GetPixels() .Select(c => Mathf.CeilToInt(grayscale * c.r)); var detailArray = new int[w, h]; Buffer.BlockCopy(grayValues.ToArray(), 0, detailArray, 0, w * h * sizeof(int)); tData.SetDetailLayer(0, 0, detailLayer, detailArray); if (!ReferenceEquals(assignableTexture, texture)) Object.DestroyImmediate(assignableTexture); } /// /// Returns a readable Texture2D from the supplied RenderTarget. /// /// an R8 format RenderTexture /// Texture2D private static Texture2D DensityMapFromRenderTexture(in RenderTexture renderTexture) { if (renderTexture.format != RenderTextureFormat.R8) throw new ArgumentException( $"DensityMapFromRenderTexture expects a single channel 8-bit renderTextures, got {renderTexture.format}"); var w = renderTexture.width; var h = renderTexture.height; var copyRect = new Rect(0, 0, w, h); var outputTexture = new Texture2D(w, h, TextureFormat.R8, false); outputTexture.ClearMinimumMipmapLevel(); outputTexture.Apply(false); using (var RTContext = new ActiveRenderTextureContext(renderTexture)) { outputTexture.ReadPixels(copyRect, 0, 0, false); outputTexture.Apply(false); } return outputTexture; } /// /// Sets a TerrainData's detail layer using the supplied RenderTextures /// This executes synchronously /// /// /// /// public static void SetDensityMap(TerrainData tData, RenderTexture rTexture, int detailLayer = 0) { if (rTexture.format != RenderTextureFormat.R8) throw new ArgumentException( $"SetDensityMap expects a single channel 8-bit renderTextures, got {rTexture.format}"); SetDensityMap(tData, DensityMapFromRenderTexture(rTexture), detailLayer); } /// /// Use the supplied material to generate a detail layer map for the supplied TerrainData /// The material will be blitted to an 8-bit R8 RenderTarget of the same resolution as /// the TerrainData's detail layers. /// The material will be given references to the TerrainData's splatmap textures /// (referenced as "_Control0" and "_Control1" if present), and the heightmap /// (referenced as "_Height"). If instanced rendering is enabled, the material /// will also be given a reference to the TerrainData's normal map ("_Normal"). /// Existing detail arrays will be passed to the material as RenderTextures /// (referenced as "_Density#" where # is the detail layer number. /// The material can use or ignore these inputs as desired. /// /// (this) target TerrainData /// a material to blit /// the detail index of the details to scatter public static void SetDensityMap(TerrainData tData, Material densityMaterial, int detailLayer = 0) { var blitMat = new Material(densityMaterial); var splat = 0; foreach (var eachAlphamapTexture in tData.alphamapTextures) { blitMat.SetTexture($"_Control{splat}", eachAlphamapTexture); splat++; } blitMat.SetTexture("_Height", tData.heightmapTexture); var detailTexturesToRelease = new List(); for (var d = 0; d < tData.detailPrototypes.Length; d++) { detailTexturesToRelease.Add(GetDensityMapTexture(tData, d)); blitMat.SetTexture($"_Density{d}", detailTexturesToRelease[d]); } foreach (var eachTerrain in Terrain.activeTerrains) if (eachTerrain.terrainData == tData) blitMat.SetTexture("_Normal", eachTerrain.normalmapTexture); // @todo: Verify that this works under all pipelines! using (var detailRT = new RenderTextureContext(tData.detailWidth, tData.detailHeight)) { using (var activeRT = new ActiveRenderTextureContext(detailRT)) { Graphics.Blit(null, detailRT, blitMat, 0); SetDensityMap(tData, detailRT, detailLayer); } } Object.DestroyImmediate(blitMat); foreach (var eachTempTexture in detailTexturesToRelease) Object.DestroyImmediate(eachTempTexture); } // Prior to 2022.2, detail arrays were limited to 16 items per cell // after that, they have a full eight bits. The following two methods // return the appropriate multipliers based on the availability // of the new API private static int InstanceCountMultiplier(TerrainData tData) { #if UNITY_2022_2_OR_NEWER return (tData.detailScatterMode == DetailScatterMode.InstanceCountMode) ? 16 : 1; #else return 16; #endif } private static int GrayscaleMultiplier(TerrainData tData) { #if UNITY_2022_2_OR_NEWER return tData.maxDetailScatterPerRes; #else return 16; #endif } /// /// Store the current active render texture, and restore it when its context closes /// private class ActiveRenderTextureContext : IDisposable { private readonly RenderTexture _cachedRenderTexture; public ActiveRenderTextureContext(RenderTexture newActiveRenderTexture = null) { _cachedRenderTexture = RenderTexture.active; if (newActiveRenderTexture is not null) RenderTexture.active = newActiveRenderTexture; } public void Dispose() { RenderTexture.active = _cachedRenderTexture; } } /// /// A RenderTexture which releases automatically when its context closes. /// private class RenderTextureContext : IDisposable { private readonly RenderTexture _tempRT; public RenderTextureContext(int width, int height) { _tempRT = RenderTexture.GetTemporary(width, height, 0, GraphicsFormat.R8_UNorm, 1); } public void Dispose() { RenderTexture.ReleaseTemporary(_tempRT); } public static implicit operator RenderTexture(RenderTextureContext ctx) { return ctx._tempRT; } } } }