ThomagicRenderer/GrassStreamer.cs

302 lines
12 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace Assets.ThoMagic.Renderer
{
/// <summary>
/// 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.
/// </summary>
public class GrassStreamer : InstanceStreamer
{
/// <summary>
/// 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.
/// </summary>
static int globalStreamingBudget = 10000;
protected List<int> grassPrefabs;
protected List<bool> grassPrefabSet;
private readonly Terrain terrain;
private readonly TerrainData terrainData;
/// <summary>
/// Used to find cells in range of the camera.
/// </summary>
private float maxDetailDistance;
private Bounds worldBounds;
private int layerCount;
private Cell[] Cells;
private float cellSizeX, cellSizeZ;
private int cellsX, cellsZ;
/// <summary>
/// A queue with a list of cells to load. Cells are loaded per frame
/// til the streaming budget is spent.
/// </summary>
private Queue<int> cellsToLoad = new Queue<int>();
/// <summary>
/// List of cells unloaded in a frame, only temporary list used to remove from loadedCells.
/// </summary>
private List<int> cellsToUnloaded = new List<int>();
//A map of loaded cells. The cell index is z * cellCountX + x.
private Dictionary<int, Cell> loadedCells = new Dictionary<int, Cell>();
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;
}
/// <summary>
/// Update loaded cells. Called by renderer. The renderer calls this once each frame.
/// </summary>
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();
}
}
/// <summary>
/// Rebuild the cell layout. Called on initialize and on certain terrain changes.
/// </summary>
/// <param name="maxDistance">Max distance where a cell is in range</param>
/// <param name="layerCount">Count of detail layers</param>
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;
}
}
}
/// <summary>
/// Is this streamer within range?
/// </summary>
/// <param name="camera">The camera to check for</param>
/// <param name="planes">Planes of the camera frustum</param>
/// <returns></returns>
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;
}
/// <summary>
/// Load cells until either all cells are loaded from the cellsToLoad list
/// or the budget is spent.
/// </summary>
/// <returns>Unspent budget</returns>
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();
}
/// <summary>
/// Remove all instances of a cell.
/// </summary>
/// <param name="cellIndex"></param>
/// <returns>The amount of deleted instances</returns>
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;
}
/// <summary>
/// Load all instances for a cell.
/// </summary>
/// <param name="cellIndex"></param>
/// <returns>The amount of loaded instances</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="camera"></param>
/// <param name="planes"></param>
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);
}
}
}
}
}
}