namespace Environment.Instancing { using UnityEngine; using System; using System.Collections.Generic; using System.Linq; /// /// Instances behaviour to generate on surfaces of terrains. /// [RequireComponent(typeof(Terrain))] public class TerrainInstancesBehaviour : InstancesBehaviour { [Serializable] public class TerrainInstancingInput { public float Density = 1f; public InstancingSettings[] Settings; } [Header("Terrain Texture Channels")] public TerrainInstancingInput FirstLayer; public TerrainInstancingInput SecondLayer; public TerrainInstancingInput ThirdLayer; public TerrainInstancingInput FourthLayer; [Header("Variance Parameters")] [Range(0f, 0.5f)] public float PositionVariance = 0.5f; [Range(0f, 0.9f)] public float ScaleVariance = 0.2f; /// /// Calculates the bounds for the instances used for culling by Unity. /// public override Bounds CalculateInstancesBounds() { var bounds = this.GetComponent().terrainData.bounds; bounds.center += this.transform.position; return bounds; } /// /// Implementation of instance data logic for instances behaviour. /// public override Dictionary> GetInstanceData() { var instancingInput = new TerrainInstancingInput[] { this.FirstLayer, this.SecondLayer, this.ThirdLayer, this.FourthLayer }; foreach (var input in instancingInput) { if (input.Settings == null) { Debug.LogError("Instancing settings should be defined the Unity Editor."); return null; } foreach (var settings in input.Settings) { if (settings.Scale <= 0f || settings.Material == null || settings.Mesh == null) { Debug.LogError("The instancing input should have a material, mesh, a scale larger than 0 and a density larger than 0."); return null; } } } var instanceData = TerrainInstanceData(this.GetComponent(), this.PositionVariance, this.ScaleVariance, instancingInput).ToArray(); var result = new Dictionary>(); for (var i = 0; i < instancingInput.Length; i++) { var dividedInstances = DivideInstanceData(instanceData[i], instancingInput[i].Settings); if (dividedInstances != null) { foreach (var (configuration, data) in dividedInstances) { result.Add(configuration, data); } } } return result; } /// /// Calculates which layer should be picked. /// static int LargestColorIndex(Color color) => (new float[] { color.r, color.g, color.b, color.a }).Select((value, index) => (value, index)).Max().index; /// /// Calculates the terrains instance data based on settings. /// static IEnumerable TerrainInstanceData(Terrain terrain, float positionVariance, float scaleVariance, params TerrainInstancingInput[] input) { var result = new List>(); for (var i = 0; i < input.Length; i++) { result.Add(new List()); } var controlTexture = terrain.terrainData.alphamapTextureCount > 0 ? terrain.terrainData.alphamapTextures[0] : null; if (controlTexture == null) { Debug.LogWarning("Control texture not defined. Defaulting to layer 1 everywhere!"); } var terrainCenter = terrain.terrainData.bounds.center; var size = terrain.terrainData.size; float width = size.x, height = size.y, length = size.z; for (var layer = 0; layer < input.Length; layer++) { var density = input[layer].Density; var settings = input[layer].Settings; if (settings.Length == 0 || (controlTexture == null && layer > 0)) { // Dont calculate points for empty layer's configurations continue; } var step = 1f / density; var pointWidth = Mathf.RoundToInt(width * density); var pointLength = Mathf.RoundToInt(length * density); for (var x = 0; x < pointWidth; x++) { for (var z = 0; z < pointLength; z++) { var xPosition = (x + 0.5f + UnityEngine.Random.Range(-positionVariance, positionVariance)) * step; var zPosition = (z + 0.5f + UnityEngine.Random.Range(-positionVariance, positionVariance)) * step; var terrainUV = new Vector2(xPosition / width, zPosition / length); var position = new Vector3() { x = xPosition, y = terrain.terrainData.GetInterpolatedHeight(terrainUV.x, terrainUV.y), z = zPosition, } - terrainCenter; var instance = new InstanceData() { TRS = Matrix4x4.TRS( position, Quaternion.identity, Vector3.one * UnityEngine.Random.Range(1 - scaleVariance, 1 + scaleVariance) ), Normal = terrain.terrainData.GetInterpolatedNormal(terrainUV.x, terrainUV.y), }; // Choose layer that is most potent. Could roll with values as weights but this is simple. var pointLayer = controlTexture == null ? 0 : LargestColorIndex(controlTexture.GetPixelBilinear(terrainUV.x, terrainUV.y)); if (layer == pointLayer) { result[layer].Add(instance); } } } } return result.Select(list => list.ToArray()); } } }