302 lines
12 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|