Files
DTrierFlood_New/Assets/FloodSWE/Scripts/TileGraph/LodTileManager.cs

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;
}
}
}
}