using System.Collections.Generic; using UnityEngine; namespace Assets.ThoMagic.Renderer { /// /// Subdivides a terrain into smaller 2d-cells in the xz-plane based on terrain settings. Only cells in range 'maxDetailDistance' of the camera /// are alive. /// public class GrassStreamer : InstanceStreamer { /// /// The limit of how many instances are created per frame. /// TODO: While the setting is global, the budget is per streamer and each /// terrain has it's own streamer. This should be a real global budget. /// static int globalStreamingBudget = 10000; protected List grassPrefabs; protected List grassPrefabSet; private readonly Terrain terrain; private readonly TerrainData terrainData; /// /// Used to find cells in range of the camera. /// private float maxDetailDistance; private Bounds worldBounds; private int layerCount; private Cell[] Cells; private float cellSizeX, cellSizeZ; private int cellsX, cellsZ; /// /// A queue with a list of cells to load. Cells are loaded per frame /// til the streaming budget is spent. /// private Queue cellsToLoad = new Queue(); /// /// List of cells unloaded in a frame, only temporary list used to remove from loadedCells. /// private List cellsToUnloaded = new List(); //A map of loaded cells. The cell index is z * cellCountX + x. private Dictionary loadedCells = new Dictionary(); public static void SetStreamingBudget(int budget) => globalStreamingBudget = budget; private static float CalculatePatchSizeX(TerrainData terrainData) => (float)((double)terrainData.detailResolutionPerPatch / (double)terrainData.detailResolution * terrainData.size.x); private static float CalculatePatchSizeZ(TerrainData terrainData) => (float)((double)terrainData.detailResolutionPerPatch / (double)terrainData.detailResolution * terrainData.size.z); public GrassStreamer( Terrain terrain) : base() { this.terrain = terrain; terrainData = terrain.terrainData; } /// /// Update loaded cells. Called by renderer. The renderer calls this once each frame. /// public void Update() { //Load cells until either all cells are loaded or budget is spent. //Returns the budget left. int streamingBudget = UpdateLoadCellsAsync(); //Go through all loaded cells and reduce 'inRangeOfAnyCamera'. //If 'inRangeOfAnyCamera' is equal or below zero unload the cell. //'inRangeOfAnyCamera' is reset to x if a camera renders and this //cell is in range. foreach (var cellIndex in loadedCells.Keys) { var cell = loadedCells[cellIndex]; //If cell is out of range and budget exist, unload. if (cell.inRangeOfAnyCamera <= 0 && streamingBudget > 0) { streamingBudget -= UnloadCell(cellIndex); cellsToUnloaded.Add(cellIndex); } --cell.inRangeOfAnyCamera; } //Remove all cells unloaded from 'loadedCells'. if (cellsToUnloaded.Count > 0) { foreach (var id in cellsToUnloaded) loadedCells.Remove(id); cellsToUnloaded.Clear(); } } /// /// Rebuild the cell layout. Called on initialize and on certain terrain changes. /// /// Max distance where a cell is in range /// Count of detail layers public void Build(float maxDistance, int layerCount) { maxDetailDistance = maxDistance; this.layerCount = layerCount; cellSizeX = CalculatePatchSizeX(terrainData); cellSizeZ = CalculatePatchSizeZ(terrainData); Vector3 position = terrain.GetPosition(); worldBounds = new Bounds(terrainData.bounds.center + position, terrainData.bounds.size); cellsX = Mathf.CeilToInt(worldBounds.size.x / cellSizeX); cellsZ = Mathf.CeilToInt(worldBounds.size.z / cellSizeZ); Cells = new Cell[cellsX * cellsZ]; float cellSizeX_2 = cellSizeX * 0.5f; float cellSizeZ_2 = cellSizeZ * 0.5f; for (int indexZ = 0; indexZ < cellsZ; ++indexZ) { for (int indexX = 0; indexX < cellsX; ++indexX) { int cellIndex = indexZ * cellsX + indexX; Cells[cellIndex] = new Cell(); float cellCenterX = indexX * cellSizeX + cellSizeX_2; float cellCenterZ = indexZ * cellSizeZ + cellSizeZ_2; Cells[cellIndex].CenterX = cellCenterX; Cells[cellIndex].CenterZ = cellCenterZ; } } } /// /// Is this streamer within range? /// /// The camera to check for /// Planes of the camera frustum /// public override bool IsInRange(Camera camera, Plane[] planes) { if (layerCount == 0) return false; if (!terrain.editorRenderFlags.HasFlag(TerrainRenderFlags.Details)) return false; var eyePos = camera.transform.position; if ((eyePos - worldBounds.ClosestPoint(eyePos)).magnitude <= Mathf.Min(camera.farClipPlane, maxDetailDistance)) return true; return false; } /// /// Load cells until either all cells are loaded from the cellsToLoad list /// or the budget is spent. /// /// Unspent budget private int UpdateLoadCellsAsync() { int streamingBudget = globalStreamingBudget; if (globalStreamingBudget <= 0 || Cells == null || Cells.Length == 0) return streamingBudget; while(streamingBudget > 0 && cellsToLoad.TryDequeue(out var cellIndex)) { streamingBudget -= LoadCell(cellIndex); } return streamingBudget; } public virtual void Recycle() { UnloadAll(); } private void UnloadAll() { if (Cells == null || Cells.Length == 0) return; for (int cellIndex = 0; cellIndex < Cells.Length; ++cellIndex) { UnloadCell(cellIndex); } loadedCells.Clear(); } /// /// Remove all instances of a cell. /// /// /// The amount of deleted instances private int UnloadCell(int cellIndex) { if (cellIndex < 0 || cellIndex >= Cells.Length || !Cells[cellIndex].IsLoaded) return 0; var cell = Cells[cellIndex]; cell.IsLoaded = false; cell.IsLoading = false; int deletedInstances = cell.loadedInstances.Count; foreach (var instanceInfo in cell.loadedInstances) RemoveInstance(instanceInfo.objectId, instanceInfo.instanceId); cell.loadedInstances.Clear(); return deletedInstances; } /// /// Load all instances for a cell. /// /// /// The amount of loaded instances private int LoadCell(int cellIndex) { if (cellIndex < 0 || cellIndex >= Cells.Length || Cells[cellIndex].IsLoaded) return 0; var cell = Cells[cellIndex]; cell.IsLoaded = true; cell.IsLoading = false; loadedCells.Add(cellIndex, cell); int x = Mathf.FloorToInt(cell.CenterX / cellSizeX); int z = Mathf.FloorToInt(cell.CenterZ / cellSizeZ); Vector3 size = terrainData.size; Vector3 terrainOffset = terrain.transform.position; Quaternion terrainRotation = terrain.transform.rotation; float detailObjectDensity = terrain.detailObjectDensity; for (int layer = 0; layer < layerCount; layer++) { var instanceTransforms = terrainData.ComputeDetailInstanceTransforms(x, z, layer, detailObjectDensity, out var _); for (int index = 0; index < instanceTransforms.Length; ++index) { var local = instanceTransforms[index]; Vector3 interpolatedNormal = terrainData.GetInterpolatedNormal((local.posX / size.x), (local.posZ / size.z)); Quaternion rotation = terrainRotation * Quaternion.FromToRotation(Vector3.up, interpolatedNormal) * Quaternion.AngleAxis((local.rotationY * 57.2957801818848f), Vector3.up); var instanceId = AddInstance(grassPrefabs[layer], new Vector3(local.posX, local.posY, local.posZ) + terrainOffset, rotation, local.scaleXZ, local.scaleY); cell.loadedInstances.Add(new InstanceInfo { instanceId = instanceId, objectId = grassPrefabs[layer] }); } } return cell.loadedInstances.Count; } /// /// Called by each camera at the start of a frame. /// Finds all cells visible in range of a camera. Either /// resets the 'inRangeOfAnyCamera' counter or adds the cell /// to the load list. /// /// /// public override void UpdateForCamera(Camera camera, Plane[] planes) { if (Cells == null || Cells.Length == 0) return; var range = Mathf.Min(camera.farClipPlane, maxDetailDistance); var rangeX = Mathf.CeilToInt((range + cellSizeX * 0.5f) / cellSizeX); var rangeZ = Mathf.CeilToInt((range + cellSizeZ * 0.5f) / cellSizeZ); var eyePos = camera.transform.position - worldBounds.min; var eyeCellX = Mathf.FloorToInt(eyePos.x / cellSizeX); var eyeCellZ = Mathf.FloorToInt(eyePos.z / cellSizeZ); for(int indexZ = eyeCellZ - rangeZ; indexZ < eyeCellZ + rangeZ; indexZ++) for (int indexX = eyeCellX - rangeX; indexX < eyeCellX + rangeX; indexX++) { if(indexZ >= 0 && indexZ < cellsZ && indexX >= 0 && indexX < cellsX) { int cellIndex = indexZ * cellsX + indexX; if (Cells[cellIndex].IsLoaded || Cells[cellIndex].IsLoading) { Cells[cellIndex].inRangeOfAnyCamera = 2; } else { Cells[cellIndex].IsLoading = true; Cells[cellIndex].inRangeOfAnyCamera = 2; cellsToLoad.Enqueue(cellIndex); } } } } } }