480 lines
14 KiB
C#
480 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace FloodSWE.TileGraph
|
|
{
|
|
public sealed class LodTileManager : MonoBehaviour
|
|
{
|
|
[Header("LOD")]
|
|
public int minLod = 1;
|
|
public int maxLod = 4;
|
|
public float baseTileSizeMeters = 1000f;
|
|
public int maxActiveTiles = 128;
|
|
|
|
[Tooltip("LOD0 is disabled by default because L0/L1 share the same world size in the plan.")]
|
|
public bool enableLod0 = false;
|
|
|
|
[Header("Test Grid")]
|
|
public bool buildTestGridOnStart = true;
|
|
public int testGridLod = 1;
|
|
public Vector2Int testGridSize = new Vector2Int(4, 4);
|
|
public int refineSteps = 2;
|
|
public int refinePerStep = 1;
|
|
public int randomSeed = 1;
|
|
|
|
[Header("Debug")]
|
|
public bool logActiveTiles = true;
|
|
public bool logAdjacencyFixes = false;
|
|
public bool logBudget = false;
|
|
|
|
private const float AdjacencyEpsilon = 0.001f;
|
|
|
|
private readonly Dictionary<TileId, TileNode> nodes = new Dictionary<TileId, TileNode>();
|
|
private readonly List<TileNode> scratchActive = new List<TileNode>();
|
|
private readonly List<TileNode> scratchSorted = new List<TileNode>();
|
|
|
|
private void Start()
|
|
{
|
|
NormalizeLodSettings();
|
|
|
|
if (!buildTestGridOnStart)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BuildTestGrid();
|
|
AssignPriorityByDistance();
|
|
RefineByPriority(refineSteps, refinePerStep);
|
|
EnforceAdjacency();
|
|
EnforceBudget();
|
|
EnforceAdjacency();
|
|
|
|
if (logActiveTiles)
|
|
{
|
|
LogActiveTilesPerLod();
|
|
}
|
|
}
|
|
|
|
public void BuildTestGrid()
|
|
{
|
|
nodes.Clear();
|
|
|
|
NormalizeLodSettings();
|
|
testGridLod = Mathf.Clamp(testGridLod, minLod, maxLod);
|
|
testGridSize = new Vector2Int(Mathf.Max(1, testGridSize.x), Mathf.Max(1, testGridSize.y));
|
|
|
|
for (int y = 0; y < testGridSize.y; y++)
|
|
{
|
|
for (int x = 0; x < testGridSize.x; x++)
|
|
{
|
|
TileId id = new TileId(testGridLod, x, y);
|
|
TileNode node = GetOrCreateNode(id);
|
|
node.Active = true;
|
|
node.Priority = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AssignPriorityByDistance()
|
|
{
|
|
GetActiveLeafNodes(scratchActive);
|
|
if (scratchActive.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float centerX = 0.0f;
|
|
float centerY = 0.0f;
|
|
for (int i = 0; i < scratchActive.Count; i++)
|
|
{
|
|
centerX += scratchActive[i].Id.X;
|
|
centerY += scratchActive[i].Id.Y;
|
|
}
|
|
|
|
centerX /= scratchActive.Count;
|
|
centerY /= scratchActive.Count;
|
|
|
|
for (int i = 0; i < scratchActive.Count; i++)
|
|
{
|
|
TileNode node = scratchActive[i];
|
|
float dx = node.Id.X - centerX;
|
|
float dy = node.Id.Y - centerY;
|
|
float dist = Mathf.Sqrt(dx * dx + dy * dy);
|
|
node.Priority = 1.0f / (1.0f + dist);
|
|
}
|
|
}
|
|
|
|
public void RefineByPriority(int steps, int perStep)
|
|
{
|
|
steps = Mathf.Max(0, steps);
|
|
perStep = Mathf.Max(0, perStep);
|
|
|
|
for (int step = 0; step < steps; step++)
|
|
{
|
|
GetActiveLeafNodes(scratchActive);
|
|
if (scratchActive.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
scratchSorted.Clear();
|
|
scratchSorted.AddRange(scratchActive);
|
|
scratchSorted.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
|
|
|
int refineCount = Mathf.Min(perStep, scratchSorted.Count);
|
|
for (int i = 0; i < refineCount; i++)
|
|
{
|
|
Refine(scratchSorted[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void EnforceAdjacency()
|
|
{
|
|
int guard = 0;
|
|
while (guard < 200)
|
|
{
|
|
guard++;
|
|
bool changed = false;
|
|
|
|
GetActiveLeafNodes(scratchActive);
|
|
for (int i = 0; i < scratchActive.Count && !changed; i++)
|
|
{
|
|
TileNode a = scratchActive[i];
|
|
for (int j = i + 1; j < scratchActive.Count; j++)
|
|
{
|
|
TileNode b = scratchActive[j];
|
|
if (!AreAdjacent(a, b))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int diff = Mathf.Abs(a.Id.Lod - b.Id.Lod);
|
|
if (diff <= 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TileNode coarse = a.Id.Lod < b.Id.Lod ? a : b;
|
|
if (Refine(coarse))
|
|
{
|
|
if (logAdjacencyFixes)
|
|
{
|
|
TileNode fine = coarse == a ? b : a;
|
|
Debug.Log($"LOD adjacency fix: refined {coarse.Id} next to {fine.Id}.");
|
|
}
|
|
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!changed)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void EnforceBudget()
|
|
{
|
|
if (maxActiveTiles <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int guard = 0;
|
|
while (CountActiveLeaves() > maxActiveTiles && guard < 200)
|
|
{
|
|
guard++;
|
|
if (!TryCoarsenLowestPriority())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void LogActiveTilesPerLod()
|
|
{
|
|
int lodCount = Mathf.Max(1, maxLod - minLod + 1);
|
|
int[] counts = new int[lodCount];
|
|
|
|
foreach (TileNode node in nodes.Values)
|
|
{
|
|
if (!node.Active || node.HasActiveChildren)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int index = node.Id.Lod - minLod;
|
|
if (index >= 0 && index < counts.Length)
|
|
{
|
|
counts[index]++;
|
|
}
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder("Active tiles per LOD: ");
|
|
for (int i = 0; i < counts.Length; i++)
|
|
{
|
|
int lod = minLod + i;
|
|
builder.Append($"L{lod}={counts[i]}");
|
|
if (i < counts.Length - 1)
|
|
{
|
|
builder.Append(" ");
|
|
}
|
|
}
|
|
|
|
Debug.Log(builder.ToString());
|
|
}
|
|
|
|
private TileNode GetOrCreateNode(TileId id)
|
|
{
|
|
if (!nodes.TryGetValue(id, out TileNode node))
|
|
{
|
|
node = new TileNode(id, GetTileSizeMeters(id.Lod), GetGridRes(id.Lod));
|
|
nodes.Add(id, node);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
private float GetTileSizeMeters(int lod)
|
|
{
|
|
if (lod <= 1)
|
|
{
|
|
return Mathf.Max(0.01f, baseTileSizeMeters);
|
|
}
|
|
|
|
float size = baseTileSizeMeters / Mathf.Pow(2.0f, lod - 1);
|
|
return Mathf.Max(0.01f, size);
|
|
}
|
|
|
|
private int GetGridRes(int lod)
|
|
{
|
|
return lod == 0 ? 128 : 256;
|
|
}
|
|
|
|
private void GetActiveLeafNodes(List<TileNode> results)
|
|
{
|
|
results.Clear();
|
|
foreach (TileNode node in nodes.Values)
|
|
{
|
|
if (node.Active && !node.HasActiveChildren)
|
|
{
|
|
results.Add(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int CountActiveLeaves()
|
|
{
|
|
int count = 0;
|
|
foreach (TileNode node in nodes.Values)
|
|
{
|
|
if (node.Active && !node.HasActiveChildren)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private bool Refine(TileNode node)
|
|
{
|
|
if (node == null || !node.Active)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (node.Id.Lod >= maxLod)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (node.Id.Lod == 0)
|
|
{
|
|
if (enableLod0)
|
|
{
|
|
Debug.LogWarning("LodTileManager: L0->L1 refinement is not implemented. Start at L1 or disable LOD0.");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (node.Children == null || node.Children.Length != 4)
|
|
{
|
|
node.Children = new TileNode[4];
|
|
}
|
|
|
|
for (int childY = 0; childY < 2; childY++)
|
|
{
|
|
for (int childX = 0; childX < 2; childX++)
|
|
{
|
|
int index = childY * 2 + childX;
|
|
TileId childId = node.Id.Child(childX, childY);
|
|
TileNode child = GetOrCreateNode(childId);
|
|
child.Parent = node;
|
|
child.Active = true;
|
|
child.Priority = node.Priority;
|
|
node.Children[index] = child;
|
|
}
|
|
}
|
|
|
|
node.Active = false;
|
|
return true;
|
|
}
|
|
|
|
private bool TryCoarsenLowestPriority()
|
|
{
|
|
CoarsenCandidate best = default;
|
|
bool hasCandidate = false;
|
|
|
|
foreach (TileNode node in nodes.Values)
|
|
{
|
|
if (node.Id.Lod < minLod)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!node.HasChildren || !AreChildrenActiveLeaves(node))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float priority = 0.0f;
|
|
for (int i = 0; i < node.Children.Length; i++)
|
|
{
|
|
priority += node.Children[i].Priority;
|
|
}
|
|
|
|
priority /= node.Children.Length;
|
|
|
|
if (!hasCandidate || priority < best.Priority)
|
|
{
|
|
best = new CoarsenCandidate(node, priority);
|
|
hasCandidate = true;
|
|
}
|
|
}
|
|
|
|
if (!hasCandidate)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool result = Coarsen(best.Parent, best.Priority);
|
|
if (result && logBudget)
|
|
{
|
|
Debug.Log($"Budget coarsen: {best.Parent.Id} priority={best.Priority:F3}.");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private bool Coarsen(TileNode parent, float priority)
|
|
{
|
|
if (parent == null || !parent.HasChildren)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < parent.Children.Length; i++)
|
|
{
|
|
TileNode child = parent.Children[i];
|
|
if (child == null || !child.Active || child.HasActiveChildren)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < parent.Children.Length; i++)
|
|
{
|
|
parent.Children[i].Active = false;
|
|
}
|
|
|
|
parent.Active = true;
|
|
parent.Priority = priority;
|
|
return true;
|
|
}
|
|
|
|
private void NormalizeLodSettings()
|
|
{
|
|
if (!enableLod0 && minLod < 1)
|
|
{
|
|
minLod = 1;
|
|
}
|
|
|
|
if (!enableLod0 && testGridLod < 1)
|
|
{
|
|
testGridLod = 1;
|
|
}
|
|
|
|
if (maxLod < minLod)
|
|
{
|
|
maxLod = minLod;
|
|
}
|
|
}
|
|
|
|
private bool AreChildrenActiveLeaves(TileNode parent)
|
|
{
|
|
if (!parent.HasChildren)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < parent.Children.Length; i++)
|
|
{
|
|
TileNode child = parent.Children[i];
|
|
if (child == null || !child.Active || child.HasActiveChildren)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool AreAdjacent(TileNode a, TileNode b)
|
|
{
|
|
GetBounds(a, out float aMinX, out float aMaxX, out float aMinY, out float aMaxY);
|
|
GetBounds(b, out float bMinX, out float bMaxX, out float bMinY, out float bMaxY);
|
|
|
|
bool xOverlap = Overlaps(aMinX, aMaxX, bMinX, bMaxX);
|
|
bool yOverlap = Overlaps(aMinY, aMaxY, bMinY, bMaxY);
|
|
|
|
bool northSouth = xOverlap && (Mathf.Abs(aMaxY - bMinY) <= AdjacencyEpsilon || Mathf.Abs(aMinY - bMaxY) <= AdjacencyEpsilon);
|
|
bool eastWest = yOverlap && (Mathf.Abs(aMaxX - bMinX) <= AdjacencyEpsilon || Mathf.Abs(aMinX - bMaxX) <= AdjacencyEpsilon);
|
|
|
|
return northSouth || eastWest;
|
|
}
|
|
|
|
private void GetBounds(TileNode node, out float minX, out float maxX, out float minY, out float maxY)
|
|
{
|
|
float size = GetTileSizeMeters(node.Id.Lod);
|
|
minX = node.Id.X * size;
|
|
maxX = minX + size;
|
|
minY = node.Id.Y * size;
|
|
maxY = minY + size;
|
|
}
|
|
|
|
private bool Overlaps(float aMin, float aMax, float bMin, float bMax)
|
|
{
|
|
return aMin < bMax - AdjacencyEpsilon && aMax > bMin + AdjacencyEpsilon;
|
|
}
|
|
|
|
private readonly struct CoarsenCandidate
|
|
{
|
|
public readonly TileNode Parent;
|
|
public readonly float Priority;
|
|
|
|
public CoarsenCandidate(TileNode parent, float priority)
|
|
{
|
|
Parent = parent;
|
|
Priority = priority;
|
|
}
|
|
}
|
|
}
|
|
}
|