602 lines
20 KiB
C#
602 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace FloodSWE.IO
|
|
{
|
|
public sealed class SweTileLoader : MonoBehaviour
|
|
{
|
|
public enum SourceMode
|
|
{
|
|
Resources = 0,
|
|
StreamingAssets = 1,
|
|
AbsolutePath = 2,
|
|
}
|
|
|
|
[Header("Tile Index")]
|
|
public TextAsset tileIndexCsv;
|
|
public SourceMode sourceMode = SourceMode.Resources;
|
|
public string tileIndexPath = "export_swe/swe_tile_index.csv";
|
|
public string fileRoot = "";
|
|
public string resourcesRoot = "export_swe";
|
|
|
|
[Header("Runtime")]
|
|
public bool cacheTextures = true;
|
|
public bool verboseDiagnostics = true;
|
|
public bool logMissingTextures = true;
|
|
|
|
private readonly Dictionary<TileKey, TileRecord> records = new Dictionary<TileKey, TileRecord>();
|
|
private readonly Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> missingTextureWarnings = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private bool indexLoaded;
|
|
private int loadAttempts;
|
|
private int loadSuccesses;
|
|
private string lastRequestedTileKey = "n/a";
|
|
private string lastLoadedTileKey = "n/a";
|
|
private string lastLoadSummary = "No tile load attempted yet.";
|
|
private bool lastLoadSucceeded;
|
|
private int indexRecordCount;
|
|
private int indexSkippedRows;
|
|
|
|
public string LastRequestedTileKey => lastRequestedTileKey;
|
|
public string LastLoadedTileKey => lastLoadedTileKey;
|
|
public string LastLoadSummary => lastLoadSummary;
|
|
public bool LastLoadSucceeded => lastLoadSucceeded;
|
|
public int IndexedTileCount => indexRecordCount;
|
|
public int IndexSkippedRows => indexSkippedRows;
|
|
public int LoadAttempts => loadAttempts;
|
|
public int LoadSuccesses => loadSuccesses;
|
|
|
|
public bool TryLoadTile(string lod, int tileX, int tileY, out SweTileData data)
|
|
{
|
|
data = default;
|
|
loadAttempts++;
|
|
lastRequestedTileKey = MakeTileKeyLabel(lod, tileX, tileY);
|
|
EnsureIndexLoaded();
|
|
|
|
TileKey key = new TileKey(lod, tileX, tileY);
|
|
if (!records.TryGetValue(key, out TileRecord record))
|
|
{
|
|
lastLoadSucceeded = false;
|
|
lastLoadSummary = $"tile={lastRequestedTileKey}; status=missing_in_index; indexed={records.Count}";
|
|
Debug.LogWarning($"SweTileLoader: {lastLoadSummary}");
|
|
return false;
|
|
}
|
|
|
|
Texture2D height = LoadTexture(record.HeightPath);
|
|
Texture2D porosity = LoadTexture(record.PorosityPath);
|
|
Texture2D buildings = LoadTexture(record.BuildingPath);
|
|
string sourcePath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "source_ids");
|
|
string sinkPath = BuildBoundaryMaskPath(record.Lod, record.X, record.Y, "sink_ids");
|
|
Texture2D sourceIds = LoadTexture(sourcePath);
|
|
Texture2D sinkIds = LoadTexture(sinkPath);
|
|
|
|
data = new SweTileData(record, height, porosity, buildings, sourceIds, sinkIds);
|
|
bool loadedAny = height != null || porosity != null || buildings != null || sourceIds != null || sinkIds != null;
|
|
lastLoadSucceeded = loadedAny;
|
|
if (loadedAny)
|
|
{
|
|
loadSuccesses++;
|
|
lastLoadedTileKey = lastRequestedTileKey;
|
|
}
|
|
|
|
lastLoadSummary = BuildLoadSummary(
|
|
lastRequestedTileKey,
|
|
record,
|
|
height,
|
|
porosity,
|
|
buildings,
|
|
sourceIds,
|
|
sinkIds,
|
|
sourcePath,
|
|
sinkPath);
|
|
|
|
if (!loadedAny)
|
|
{
|
|
Debug.LogWarning($"SweTileLoader: {lastLoadSummary}");
|
|
}
|
|
else if (verboseDiagnostics)
|
|
{
|
|
Debug.Log($"SweTileLoader: {lastLoadSummary}");
|
|
}
|
|
|
|
return loadedAny;
|
|
}
|
|
|
|
public bool ApplyToSimulator(string lod, int tileX, int tileY, SweTileSimulator simulator)
|
|
{
|
|
if (simulator == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryLoadTile(lod, tileX, tileY, out SweTileData data))
|
|
{
|
|
Debug.LogWarning($"SweTileLoader: failed to load tile {lod} ({tileX},{tileY}).");
|
|
return false;
|
|
}
|
|
|
|
if (data.Height != null)
|
|
{
|
|
simulator.terrainHeight = data.Height;
|
|
}
|
|
if (data.Porosity != null)
|
|
{
|
|
simulator.porosity = data.Porosity;
|
|
}
|
|
simulator.ApplyTileStaticData(
|
|
data.Height,
|
|
data.Porosity,
|
|
data.SourceIds,
|
|
data.SinkIds,
|
|
data.Resolution,
|
|
data.TileSizeM);
|
|
|
|
if (verboseDiagnostics)
|
|
{
|
|
Debug.Log(
|
|
$"SweTileLoader: applied tile {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
|
|
$"to simulator (res={data.Resolution}, tileSize={data.TileSizeM:0.###}m, " +
|
|
$"height={DescribeTexture(data.Height)}, porosity={DescribeTexture(data.Porosity)}, " +
|
|
$"sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)}).");
|
|
}
|
|
|
|
if (data.SourceIds == null || data.SinkIds == null)
|
|
{
|
|
Debug.LogWarning(
|
|
$"SweTileLoader: boundary mask coverage is incomplete for {MakeTileKeyLabel(data.Lod, data.TileX, data.TileY)} " +
|
|
$"(sourceMask={DescribeTexture(data.SourceIds)}, sinkMask={DescribeTexture(data.SinkIds)}).");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string BuildBoundaryMaskPath(string lod, int tileX, int tileY, string kind)
|
|
{
|
|
return $"export_swe/{lod}/{kind}/{kind}_{tileX}_{tileY}.exr";
|
|
}
|
|
|
|
private void EnsureIndexLoaded()
|
|
{
|
|
if (indexLoaded)
|
|
{
|
|
return;
|
|
}
|
|
|
|
records.Clear();
|
|
indexRecordCount = 0;
|
|
indexSkippedRows = 0;
|
|
|
|
string text = tileIndexCsv != null ? tileIndexCsv.text : LoadTextFromPath();
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
Debug.LogWarning("SweTileLoader: tile index CSV is empty.");
|
|
indexLoaded = true;
|
|
return;
|
|
}
|
|
|
|
string[] lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (lines.Length <= 1)
|
|
{
|
|
indexLoaded = true;
|
|
return;
|
|
}
|
|
|
|
int headerColumns = 0;
|
|
for (int i = 0; i < lines.Length; i++)
|
|
{
|
|
string line = lines[i].Trim();
|
|
if (line.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string[] parts = line.Split(',');
|
|
if (i == 0)
|
|
{
|
|
headerColumns = parts.Length;
|
|
continue;
|
|
}
|
|
|
|
if (parts.Length < headerColumns)
|
|
{
|
|
indexSkippedRows++;
|
|
continue;
|
|
}
|
|
if (parts.Length <= 11)
|
|
{
|
|
indexSkippedRows++;
|
|
continue;
|
|
}
|
|
|
|
string lod = parts[0].Trim();
|
|
if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileX))
|
|
{
|
|
indexSkippedRows++;
|
|
continue;
|
|
}
|
|
if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tileY))
|
|
{
|
|
indexSkippedRows++;
|
|
continue;
|
|
}
|
|
|
|
TileKey key = new TileKey(lod, tileX, tileY);
|
|
records[key] = new TileRecord(
|
|
lod,
|
|
tileX,
|
|
tileY,
|
|
ParseFloat(parts[7], 1000.0f),
|
|
ParseInt(parts[8], 256),
|
|
NormalizePath(parts[9]),
|
|
NormalizePath(parts[10]),
|
|
NormalizePath(parts[11])
|
|
);
|
|
}
|
|
|
|
indexRecordCount = records.Count;
|
|
indexLoaded = true;
|
|
if (verboseDiagnostics)
|
|
{
|
|
Debug.Log(
|
|
$"SweTileLoader: tile index loaded (records={indexRecordCount}, skipped={indexSkippedRows}, " +
|
|
$"source={DescribeIndexSource()}).");
|
|
}
|
|
}
|
|
|
|
private string LoadTextFromPath()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tileIndexPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (sourceMode == SourceMode.Resources)
|
|
{
|
|
string resourcePath = ToResourcesPath(tileIndexPath);
|
|
TextAsset resourceCsv = Resources.Load<TextAsset>(resourcePath);
|
|
if (resourceCsv != null)
|
|
{
|
|
return resourceCsv.text;
|
|
}
|
|
|
|
Debug.LogWarning($"SweTileLoader: Resources CSV not found at '{resourcePath}'.");
|
|
}
|
|
|
|
string path = tileIndexPath;
|
|
if (sourceMode == SourceMode.StreamingAssets)
|
|
{
|
|
path = Path.Combine(Application.streamingAssetsPath, tileIndexPath);
|
|
}
|
|
|
|
if (!File.Exists(path))
|
|
{
|
|
Debug.LogWarning($"SweTileLoader: CSV not found at {path}");
|
|
return null;
|
|
}
|
|
|
|
return File.ReadAllText(path);
|
|
}
|
|
|
|
private Texture2D LoadTexture(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (cacheTextures && textureCache.TryGetValue(path, out Texture2D cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
Texture2D texture = null;
|
|
switch (sourceMode)
|
|
{
|
|
case SourceMode.Resources:
|
|
string resourcePath = ToResourcesPath(path);
|
|
texture = Resources.Load<Texture2D>(resourcePath);
|
|
if (texture == null)
|
|
{
|
|
LogMissingTextureOnce(path, $"Resources/{resourcePath}");
|
|
}
|
|
break;
|
|
case SourceMode.StreamingAssets:
|
|
case SourceMode.AbsolutePath:
|
|
texture = LoadTextureFromFile(path);
|
|
break;
|
|
}
|
|
|
|
if (cacheTextures && texture != null)
|
|
{
|
|
textureCache[path] = texture;
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
private string NormalizePath(string raw)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
string path = raw.Trim().Replace("\\", "/");
|
|
if (sourceMode == SourceMode.AbsolutePath)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (sourceMode == SourceMode.StreamingAssets)
|
|
{
|
|
return string.IsNullOrWhiteSpace(fileRoot) ? path : Path.Combine(fileRoot, path).Replace("\\", "/");
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private string ToResourcesPath(string path)
|
|
{
|
|
string normalized = path.Replace("\\", "/");
|
|
if (!string.IsNullOrWhiteSpace(resourcesRoot))
|
|
{
|
|
int idx = normalized.IndexOf(resourcesRoot, StringComparison.OrdinalIgnoreCase);
|
|
if (idx >= 0)
|
|
{
|
|
normalized = normalized.Substring(idx);
|
|
}
|
|
}
|
|
|
|
if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
normalized = normalized.Substring("Assets/".Length);
|
|
}
|
|
|
|
if (normalized.StartsWith("Resources/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
normalized = normalized.Substring("Resources/".Length);
|
|
}
|
|
|
|
int dot = normalized.LastIndexOf('.');
|
|
int slash = normalized.LastIndexOf('/');
|
|
if (dot > slash)
|
|
{
|
|
normalized = normalized.Substring(0, dot);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private Texture2D LoadTextureFromFile(string path)
|
|
{
|
|
string resolved = path;
|
|
if (sourceMode == SourceMode.StreamingAssets)
|
|
{
|
|
resolved = Path.Combine(Application.streamingAssetsPath, path);
|
|
}
|
|
if (!File.Exists(resolved))
|
|
{
|
|
LogMissingTextureOnce(path, resolved);
|
|
return null;
|
|
}
|
|
|
|
byte[] bytes = File.ReadAllBytes(resolved);
|
|
Texture2D texture = new Texture2D(2, 2, TextureFormat.RFloat, false, true);
|
|
if (!ImageConversion.LoadImage(texture, bytes, false))
|
|
{
|
|
Destroy(texture);
|
|
Debug.LogWarning($"SweTileLoader: failed to decode {resolved}");
|
|
return null;
|
|
}
|
|
|
|
texture.wrapMode = TextureWrapMode.Clamp;
|
|
texture.filterMode = FilterMode.Point;
|
|
texture.name = Path.GetFileNameWithoutExtension(resolved);
|
|
return texture;
|
|
}
|
|
|
|
private string BuildLoadSummary(
|
|
string requestedTile,
|
|
in TileRecord record,
|
|
Texture2D height,
|
|
Texture2D porosity,
|
|
Texture2D buildings,
|
|
Texture2D sourceIds,
|
|
Texture2D sinkIds,
|
|
string sourcePath,
|
|
string sinkPath)
|
|
{
|
|
var sb = new StringBuilder(256);
|
|
sb.Append("tile=").Append(requestedTile)
|
|
.Append("; index=").Append(indexRecordCount)
|
|
.Append("; res=").Append(record.Resolution)
|
|
.Append("; tileSize=").Append(record.TileSizeM.ToString("0.###", CultureInfo.InvariantCulture))
|
|
.Append("; height=").Append(DescribeTexture(height))
|
|
.Append("; porosity=").Append(DescribeTexture(porosity))
|
|
.Append("; buildings=").Append(DescribeTexture(buildings))
|
|
.Append("; sourceMask=").Append(DescribeTexture(sourceIds))
|
|
.Append(" (").Append(sourcePath).Append(")")
|
|
.Append("; sinkMask=").Append(DescribeTexture(sinkIds))
|
|
.Append(" (").Append(sinkPath).Append(")")
|
|
.Append("; loads=").Append(loadSuccesses).Append("/").Append(loadAttempts);
|
|
return sb.ToString();
|
|
}
|
|
|
|
private void LogMissingTextureOnce(string requestedPath, string resolvedPath)
|
|
{
|
|
if (!logMissingTextures)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!missingTextureWarnings.Add(resolvedPath ?? requestedPath ?? "unknown"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Debug.LogWarning(
|
|
$"SweTileLoader: texture not found (requested='{requestedPath}', resolved='{resolvedPath}', mode={sourceMode}).");
|
|
}
|
|
|
|
private string DescribeIndexSource()
|
|
{
|
|
if (tileIndexCsv != null)
|
|
{
|
|
return $"TextAsset:{tileIndexCsv.name}";
|
|
}
|
|
|
|
if (sourceMode == SourceMode.StreamingAssets)
|
|
{
|
|
return $"StreamingAssets/{tileIndexPath}";
|
|
}
|
|
|
|
if (sourceMode == SourceMode.AbsolutePath)
|
|
{
|
|
return tileIndexPath;
|
|
}
|
|
|
|
return $"Resources/{ToResourcesPath(tileIndexPath)}";
|
|
}
|
|
|
|
private static string MakeTileKeyLabel(string lod, int x, int y)
|
|
{
|
|
string normalized = string.IsNullOrWhiteSpace(lod) ? "lod?" : lod.Trim();
|
|
return $"{normalized} ({x},{y})";
|
|
}
|
|
|
|
private static string DescribeTexture(Texture2D texture)
|
|
{
|
|
if (texture == null)
|
|
{
|
|
return "missing";
|
|
}
|
|
|
|
return $"{texture.width}x{texture.height},readable={texture.isReadable}";
|
|
}
|
|
|
|
private static float ParseFloat(string raw, float fallback)
|
|
{
|
|
if (float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
private static int ParseInt(string raw, int fallback)
|
|
{
|
|
if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
private readonly struct TileKey : IEquatable<TileKey>
|
|
{
|
|
public readonly string Lod;
|
|
public readonly int X;
|
|
public readonly int Y;
|
|
|
|
public TileKey(string lod, int x, int y)
|
|
{
|
|
Lod = lod ?? "";
|
|
X = x;
|
|
Y = y;
|
|
}
|
|
|
|
public bool Equals(TileKey other)
|
|
{
|
|
return X == other.X && Y == other.Y && string.Equals(Lod, other.Lod, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
return obj is TileKey other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
unchecked
|
|
{
|
|
int hash = 17;
|
|
hash = hash * 31 + X;
|
|
hash = hash * 31 + Y;
|
|
hash = hash * 31 + StringComparer.OrdinalIgnoreCase.GetHashCode(Lod);
|
|
return hash;
|
|
}
|
|
}
|
|
}
|
|
|
|
public readonly struct TileRecord
|
|
{
|
|
public readonly string Lod;
|
|
public readonly int X;
|
|
public readonly int Y;
|
|
public readonly float TileSizeM;
|
|
public readonly int Resolution;
|
|
public readonly string HeightPath;
|
|
public readonly string PorosityPath;
|
|
public readonly string BuildingPath;
|
|
|
|
public TileRecord(
|
|
string lod,
|
|
int x,
|
|
int y,
|
|
float tileSizeM,
|
|
int resolution,
|
|
string heightPath,
|
|
string porosityPath,
|
|
string buildingPath)
|
|
{
|
|
Lod = lod;
|
|
X = x;
|
|
Y = y;
|
|
TileSizeM = tileSizeM;
|
|
Resolution = resolution;
|
|
HeightPath = heightPath;
|
|
PorosityPath = porosityPath;
|
|
BuildingPath = buildingPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
public readonly struct SweTileData
|
|
{
|
|
public readonly string Lod;
|
|
public readonly int TileX;
|
|
public readonly int TileY;
|
|
public readonly float TileSizeM;
|
|
public readonly int Resolution;
|
|
public readonly Texture2D Height;
|
|
public readonly Texture2D Porosity;
|
|
public readonly Texture2D Buildings;
|
|
public readonly Texture2D SourceIds;
|
|
public readonly Texture2D SinkIds;
|
|
|
|
public SweTileData(
|
|
in SweTileLoader.TileRecord record,
|
|
Texture2D height,
|
|
Texture2D porosity,
|
|
Texture2D buildings,
|
|
Texture2D sourceIds,
|
|
Texture2D sinkIds)
|
|
{
|
|
Lod = record.Lod;
|
|
TileX = record.X;
|
|
TileY = record.Y;
|
|
TileSizeM = record.TileSizeM;
|
|
Resolution = record.Resolution;
|
|
Height = height;
|
|
Porosity = porosity;
|
|
Buildings = buildings;
|
|
SourceIds = sourceIds;
|
|
SinkIds = sinkIds;
|
|
}
|
|
}
|
|
}
|