Add SWE LOD exports and improve Unity importers
- add swe_lod config and exporter for height/porosity/building EXRs - write river mask PNGs and add tree water/bridge masking - update Unity importers with tile selection, ortho fallback, and building prefabs
This commit is contained in:
@@ -16,6 +16,7 @@ public class GeoTileImporter : EditorWindow
|
||||
private string tilesCsvPath = "Assets/GeoData/tile_index.csv";
|
||||
private string heightmapsDir = "Assets/GeoData/height_png16";
|
||||
private string orthoDir = "Assets/GeoData/ortho_jpg";
|
||||
private string orthoDirFallback = "Assets/GeoData/ortho_jpg_river";
|
||||
private string buildingsDir = "Assets/GeoData/buildings_tiles";
|
||||
private string buildingsEnhancedDir = "Assets/GeoData/buildings_enhanced";
|
||||
private string treesDir = "Assets/GeoData/trees_tiles";
|
||||
@@ -34,6 +35,7 @@ public class GeoTileImporter : EditorWindow
|
||||
private bool applyOrthoTextures = true;
|
||||
private bool importBuildings = true;
|
||||
private bool useEnhancedBuildings = false;
|
||||
private bool importBuildingsEvenTilesOnly = true;
|
||||
private bool importTrees = true;
|
||||
private bool importFurniture = false;
|
||||
private bool deleteExistingBuildings = false;
|
||||
@@ -43,6 +45,24 @@ public class GeoTileImporter : EditorWindow
|
||||
private bool deleteExistingEnhancedTrees = false;
|
||||
private string enhancedTreesParentName = "Geo_Trees_Enhanced";
|
||||
|
||||
private Vector2 scrollPosition;
|
||||
private float tileKeySizeX = 1000f;
|
||||
private float tileKeySizeY = 1000f;
|
||||
private float tileKeyOverlapX = 0.5f;
|
||||
private float tileKeyOverlapY = 0.5f;
|
||||
private string bottomLeftKey = "";
|
||||
private string topRightKey = "";
|
||||
private readonly List<TileRecord> tileIndexCache = new List<TileRecord>();
|
||||
private readonly List<TileKeyOption> tileKeyOptions = new List<TileKeyOption>();
|
||||
private readonly Dictionary<string, TileKeyOption> tileKeyLookup = new Dictionary<string, TileKeyOption>(StringComparer.OrdinalIgnoreCase);
|
||||
private string[] tileKeyStrings = Array.Empty<string>();
|
||||
private string cachedTileCsvPath = "";
|
||||
private DateTime cachedTileCsvTimeUtc = DateTime.MinValue;
|
||||
private float cachedTileKeySizeX = 0f;
|
||||
private float cachedTileKeySizeY = 0f;
|
||||
private float cachedTileOverlapX = 0f;
|
||||
private float cachedTileOverlapY = 0f;
|
||||
|
||||
// Prefabs for trees and furniture (assign in editor)
|
||||
private GameObject treePrefab;
|
||||
private GameObject lampPrefab;
|
||||
@@ -60,10 +80,13 @@ public class GeoTileImporter : EditorWindow
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
GUILayout.Label("Inputs", EditorStyles.boldLabel);
|
||||
tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath);
|
||||
heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir);
|
||||
orthoDir = EditorGUILayout.TextField("ortho_jpg dir", orthoDir);
|
||||
orthoDir = EditorGUILayout.TextField("ortho_jpg dir (primary)", orthoDir);
|
||||
orthoDirFallback = EditorGUILayout.TextField("ortho_jpg dir (fallback)", orthoDirFallback);
|
||||
buildingsDir = EditorGUILayout.TextField("buildings_glb dir", buildingsDir);
|
||||
treesDir = EditorGUILayout.TextField("trees_glb dir", treesDir);
|
||||
treeProxyPath = EditorGUILayout.TextField("tree_proxies.glb", treeProxyPath);
|
||||
@@ -83,6 +106,7 @@ public class GeoTileImporter : EditorWindow
|
||||
deleteExistingBuildings = EditorGUILayout.ToggleLeft("Delete existing buildings under parent", deleteExistingBuildings);
|
||||
importBuildings = EditorGUILayout.ToggleLeft("Import buildings (GLB per tile)", importBuildings);
|
||||
useEnhancedBuildings = EditorGUILayout.ToggleLeft("Use enhanced buildings (from buildings_enhanced/)", useEnhancedBuildings);
|
||||
importBuildingsEvenTilesOnly = EditorGUILayout.ToggleLeft("Import 2km buildings once (even X/Y tiles only)", importBuildingsEvenTilesOnly);
|
||||
|
||||
GUILayout.Space(5);
|
||||
treesParentName = EditorGUILayout.TextField("Trees parent name", treesParentName);
|
||||
@@ -110,15 +134,30 @@ public class GeoTileImporter : EditorWindow
|
||||
bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
|
||||
defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture Prefab", defaultFurniturePrefab, typeof(GameObject), false);
|
||||
|
||||
GUILayout.Space(12);
|
||||
GUILayout.Label("Tile Key Config", EditorStyles.boldLabel);
|
||||
tileKeySizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileKeySizeX);
|
||||
tileKeySizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileKeySizeY);
|
||||
tileKeyOverlapX = EditorGUILayout.FloatField("Overlap X (m)", tileKeyOverlapX);
|
||||
tileKeyOverlapY = EditorGUILayout.FloatField("Overlap Y (m)", tileKeyOverlapY);
|
||||
|
||||
RefreshTileIndexCache();
|
||||
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Tile Selection", EditorStyles.boldLabel);
|
||||
DrawTileSelectionUI();
|
||||
|
||||
GUILayout.Space(12);
|
||||
if (GUILayout.Button("Import / Rebuild"))
|
||||
ImportTiles();
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"Creates one Unity Terrain per CSV row and positions tiles on a meter grid.\n" +
|
||||
"Absolute elevation mapping: Terrain Y = tile_min (or global_min), Terrain height = (tile_max - tile_min).\n" +
|
||||
"Absolute elevation mapping: Terrain Y = tile_min (fallback global_min), Terrain height = (tile_max - tile_min).\n" +
|
||||
"CSV is header-driven (order-independent). Optionally applies ortho JPGs and instantiates buildings/trees GLBs.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private static void EnsureHeightmapImportSettings(string assetPath)
|
||||
@@ -252,12 +291,19 @@ public class GeoTileImporter : EditorWindow
|
||||
Debug.LogError($"[GeoTileImporter] Heightmap dir not found: {heightmapsDir}");
|
||||
return;
|
||||
}
|
||||
if (applyOrthoTextures && !Directory.Exists(orthoDir))
|
||||
if (applyOrthoTextures)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Ortho dir not found: {orthoDir} (textures will be skipped).");
|
||||
applyOrthoTextures = false;
|
||||
bool primaryExists = Directory.Exists(orthoDir);
|
||||
bool fallbackExists = !string.IsNullOrWhiteSpace(orthoDirFallback) && Directory.Exists(orthoDirFallback);
|
||||
if (!primaryExists && !fallbackExists)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Ortho dirs not found: primary={orthoDir}, fallback={orthoDirFallback} (textures will be skipped).");
|
||||
applyOrthoTextures = false;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshTileIndexCache();
|
||||
|
||||
var parent = GameObject.Find(parentName);
|
||||
if (parent == null) parent = new GameObject(parentName);
|
||||
|
||||
@@ -267,160 +313,58 @@ public class GeoTileImporter : EditorWindow
|
||||
DestroyImmediate(parent.transform.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
var lines = File.ReadAllLines(tilesCsvPath);
|
||||
|
||||
Debug.Log($"[GeoTileImporter] Read {lines.Length} lines.");
|
||||
if (lines.Length < 2)
|
||||
var tiles = ParseTilesCsv();
|
||||
if (tiles == null || tiles.Count == 0)
|
||||
{
|
||||
Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row).");
|
||||
Debug.LogError("[GeoTileImporter] No valid tiles found in CSV.");
|
||||
return;
|
||||
}
|
||||
|
||||
var headerLine = lines[0].Trim();
|
||||
var headerMap = BuildHeaderMap(headerLine);
|
||||
Debug.Log($"[GeoTileImporter] Header: {headerLine}");
|
||||
Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}");
|
||||
|
||||
// Required columns (order-independent)
|
||||
string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" };
|
||||
if (!HasAll(headerMap, required))
|
||||
var selectedTiles = ApplySelection(tiles);
|
||||
if (selectedTiles.Count == 0)
|
||||
{
|
||||
Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " +
|
||||
string.Join(", ", required) +
|
||||
"\nFound: " + string.Join(", ", headerMap.Keys));
|
||||
Debug.LogError("[GeoTileImporter] Selection is empty. Adjust Tile Selection and try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
int IDX_TILE = headerMap["tile_id"];
|
||||
int IDX_XMIN = headerMap["xmin"];
|
||||
int IDX_YMIN = headerMap["ymin"];
|
||||
int IDX_GMIN = headerMap["global_min"];
|
||||
int IDX_GMAX = headerMap["global_max"];
|
||||
int IDX_RES = headerMap["out_res"];
|
||||
bool hasTileMin = headerMap.TryGetValue("tile_min", out int IDX_TMIN);
|
||||
bool hasTileMax = headerMap.TryGetValue("tile_max", out int IDX_TMAX);
|
||||
bool useTileRange = hasTileMin && hasTileMax;
|
||||
|
||||
// Compute origin from min xmin/ymin
|
||||
double originX = double.PositiveInfinity;
|
||||
double originY = double.PositiveInfinity;
|
||||
|
||||
int validRowsForOrigin = 0;
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
for (int i = 0; i < selectedTiles.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
// Robust: just ensure indices exist in this row
|
||||
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
|
||||
if (useTileRange)
|
||||
needMaxIndex = Math.Max(needMaxIndex, Math.Max(IDX_TMIN, IDX_TMAX));
|
||||
if (parts.Length <= needMaxIndex)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Origin scan: skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double xmin = double.Parse(parts[IDX_XMIN], ci);
|
||||
double ymin = double.Parse(parts[IDX_YMIN], ci);
|
||||
originX = Math.Min(originX, xmin);
|
||||
originY = Math.Min(originY, ymin);
|
||||
validRowsForOrigin++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Origin scan parse failed line {i + 1}: '{line}'\n{e.Message}");
|
||||
}
|
||||
originX = Math.Min(originX, selectedTiles[i].Xmin);
|
||||
originY = Math.Min(originY, selectedTiles[i].Ymin);
|
||||
}
|
||||
|
||||
if (validRowsForOrigin == 0 || double.IsInfinity(originX) || double.IsInfinity(originY))
|
||||
if (double.IsInfinity(originX) || double.IsInfinity(originY))
|
||||
{
|
||||
Debug.LogError("[GeoTileImporter] Could not compute origin (no valid rows parsed). Check CSV numeric format.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {validRowsForOrigin} valid rows.");
|
||||
Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {selectedTiles.Count} selected tiles.");
|
||||
|
||||
int imported = 0, skipped = 0;
|
||||
int importedTextures = 0;
|
||||
var placements = new List<(string tileId, float ux, float uz, float baseMin)>();
|
||||
var placements = new List<(string tileId, float ux, float uz, float baseY)>();
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
for (int i = 0; i < selectedTiles.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
var tile = selectedTiles[i];
|
||||
var tileId = tile.TileId;
|
||||
double xmin = tile.Xmin;
|
||||
double ymin = tile.Ymin;
|
||||
double baseMin = tile.TileMin;
|
||||
double baseMax = tile.TileMax;
|
||||
|
||||
var parts = line.Split(',');
|
||||
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
|
||||
if (useTileRange)
|
||||
needMaxIndex = Math.Max(needMaxIndex, Math.Max(IDX_TMIN, IDX_TMAX));
|
||||
if (parts.Length <= needMaxIndex)
|
||||
{
|
||||
skipped++;
|
||||
Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
|
||||
continue;
|
||||
}
|
||||
if (tile.OutRes != heightmapResolution)
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={tile.OutRes} but importer expects {heightmapResolution}.");
|
||||
|
||||
string tileId = parts[IDX_TILE].Trim();
|
||||
|
||||
double xmin, ymin, gmin, gmax;
|
||||
double tileMin = 0.0;
|
||||
double tileMax = 0.0;
|
||||
bool tileRangeValid = false;
|
||||
int outRes;
|
||||
try
|
||||
{
|
||||
xmin = double.Parse(parts[IDX_XMIN], ci);
|
||||
ymin = double.Parse(parts[IDX_YMIN], ci);
|
||||
gmin = double.Parse(parts[IDX_GMIN], ci);
|
||||
gmax = double.Parse(parts[IDX_GMAX], ci);
|
||||
outRes = int.Parse(parts[IDX_RES], ci);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
skipped++;
|
||||
Debug.LogWarning($"[GeoTileImporter] Parse failed line {i + 1} tile '{tileId}': {e.Message}\nLine: '{line}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (useTileRange)
|
||||
{
|
||||
if (double.TryParse(parts[IDX_TMIN], NumberStyles.Float, ci, out double tmin) &&
|
||||
double.TryParse(parts[IDX_TMAX], NumberStyles.Float, ci, out double tmax))
|
||||
{
|
||||
tileMin = tmin;
|
||||
tileMax = tmax;
|
||||
tileRangeValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid tile_min/tile_max; falling back to global range.");
|
||||
}
|
||||
}
|
||||
|
||||
if (outRes != heightmapResolution)
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={outRes} but importer expects {heightmapResolution}.");
|
||||
|
||||
double baseMin = tileRangeValid ? tileMin : gmin;
|
||||
double baseMax = tileRangeValid ? tileMax : gmax;
|
||||
float heightRange = (float)(baseMax - baseMin);
|
||||
if (heightRange <= 0.0001f)
|
||||
{
|
||||
if (tileRangeValid)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: flat tile range; using epsilon height range.");
|
||||
heightRange = 0.001f;
|
||||
}
|
||||
else
|
||||
{
|
||||
skipped++;
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (global_max <= global_min). Skipping.");
|
||||
continue;
|
||||
}
|
||||
skipped++;
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (tile_max <= tile_min). Skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
string pngPath = Path.Combine(heightmapsDir, $"{tileId}.png").Replace("\\", "/");
|
||||
@@ -496,6 +440,12 @@ public class GeoTileImporter : EditorWindow
|
||||
if (applyOrthoTextures)
|
||||
{
|
||||
string orthoPath = Path.Combine(orthoDir, $"{tileId}.jpg").Replace("\\", "/");
|
||||
if (!File.Exists(orthoPath) && !string.IsNullOrWhiteSpace(orthoDirFallback))
|
||||
{
|
||||
string fallbackPath = Path.Combine(orthoDirFallback, $"{tileId}.jpg").Replace("\\", "/");
|
||||
if (File.Exists(fallbackPath))
|
||||
orthoPath = fallbackPath;
|
||||
}
|
||||
if (File.Exists(orthoPath))
|
||||
{
|
||||
EnsureOrthoImportSettings(orthoPath);
|
||||
@@ -528,7 +478,7 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTileImporter] Imported {tileId} @ XZ=({ux},{uz}) Y={baseMin} heightRange={heightRange} usedU16={usedU16}");
|
||||
Debug.Log($"[GeoTileImporter] Imported {tileId} ({tile.TileKey}) @ XZ=({ux},{uz}) Y={baseMin} heightRange={heightRange} usedU16={usedU16}");
|
||||
imported++;
|
||||
placements.Add((tileId, ux, uz, (float)baseMin));
|
||||
}
|
||||
@@ -544,7 +494,384 @@ public class GeoTileImporter : EditorWindow
|
||||
ImportEnhancedTrees(placements);
|
||||
}
|
||||
|
||||
private void ImportBuildings(List<(string tileId, float ux, float uz, float baseMin)> placements)
|
||||
private void RefreshTileIndexCache()
|
||||
{
|
||||
if (!File.Exists(tilesCsvPath))
|
||||
{
|
||||
tileIndexCache.Clear();
|
||||
tileKeyOptions.Clear();
|
||||
tileKeyLookup.Clear();
|
||||
tileKeyStrings = Array.Empty<string>();
|
||||
return;
|
||||
}
|
||||
|
||||
var writeTime = File.GetLastWriteTimeUtc(tilesCsvPath);
|
||||
if (tilesCsvPath == cachedTileCsvPath &&
|
||||
writeTime == cachedTileCsvTimeUtc &&
|
||||
Mathf.Approximately(tileKeySizeX, cachedTileKeySizeX) &&
|
||||
Mathf.Approximately(tileKeySizeY, cachedTileKeySizeY) &&
|
||||
Mathf.Approximately(tileKeyOverlapX, cachedTileOverlapX) &&
|
||||
Mathf.Approximately(tileKeyOverlapY, cachedTileOverlapY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tileIndexCache.Clear();
|
||||
tileIndexCache.AddRange(ParseTilesCsv());
|
||||
BuildTileKeyOptions();
|
||||
|
||||
cachedTileCsvPath = tilesCsvPath;
|
||||
cachedTileCsvTimeUtc = writeTime;
|
||||
cachedTileKeySizeX = tileKeySizeX;
|
||||
cachedTileKeySizeY = tileKeySizeY;
|
||||
cachedTileOverlapX = tileKeyOverlapX;
|
||||
cachedTileOverlapY = tileKeyOverlapY;
|
||||
}
|
||||
|
||||
private void BuildTileKeyOptions()
|
||||
{
|
||||
tileKeyOptions.Clear();
|
||||
tileKeyLookup.Clear();
|
||||
|
||||
for (int i = 0; i < tileIndexCache.Count; i++)
|
||||
{
|
||||
var tile = tileIndexCache[i];
|
||||
if (string.IsNullOrWhiteSpace(tile.TileKey))
|
||||
continue;
|
||||
|
||||
if (tileKeyLookup.ContainsKey(tile.TileKey))
|
||||
continue;
|
||||
|
||||
var option = new TileKeyOption
|
||||
{
|
||||
Key = tile.TileKey,
|
||||
X = tile.XKey,
|
||||
Y = tile.YKey
|
||||
};
|
||||
|
||||
tileKeyOptions.Add(option);
|
||||
tileKeyLookup[tile.TileKey] = option;
|
||||
}
|
||||
|
||||
tileKeyOptions.Sort((a, b) =>
|
||||
{
|
||||
int cmp = a.X.CompareTo(b.X);
|
||||
return cmp != 0 ? cmp : a.Y.CompareTo(b.Y);
|
||||
});
|
||||
|
||||
tileKeyStrings = new string[tileKeyOptions.Count];
|
||||
for (int i = 0; i < tileKeyOptions.Count; i++)
|
||||
tileKeyStrings[i] = tileKeyOptions[i].Key;
|
||||
}
|
||||
|
||||
private void DrawTileSelectionUI()
|
||||
{
|
||||
if (tileKeyOptions.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("No tile keys available. Check the tile index CSV and key config.", MessageType.Info);
|
||||
EditorGUILayout.LabelField("Tiles in selection", "0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(bottomLeftKey) || !tileKeyLookup.ContainsKey(bottomLeftKey))
|
||||
bottomLeftKey = tileKeyOptions[0].Key;
|
||||
|
||||
var bottomLeft = tileKeyLookup[bottomLeftKey];
|
||||
var topRightOptions = GetTopRightOptions(bottomLeft);
|
||||
if (topRightOptions.Count == 0)
|
||||
topRightOptions.Add(bottomLeft);
|
||||
|
||||
if (string.IsNullOrEmpty(topRightKey) || !ContainsKey(topRightOptions, topRightKey))
|
||||
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
|
||||
|
||||
int bottomLeftIndex = Array.IndexOf(tileKeyStrings, bottomLeftKey);
|
||||
int newBottomLeftIndex = EditorGUILayout.Popup("Bottom-Left Key", bottomLeftIndex, tileKeyStrings);
|
||||
if (newBottomLeftIndex != bottomLeftIndex)
|
||||
{
|
||||
bottomLeftKey = tileKeyStrings[newBottomLeftIndex];
|
||||
bottomLeft = tileKeyLookup[bottomLeftKey];
|
||||
topRightOptions = GetTopRightOptions(bottomLeft);
|
||||
if (topRightOptions.Count == 0)
|
||||
topRightOptions.Add(bottomLeft);
|
||||
|
||||
if (!ContainsKey(topRightOptions, topRightKey))
|
||||
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
|
||||
}
|
||||
|
||||
var topRightKeys = BuildKeyArray(topRightOptions);
|
||||
int topRightIndex = Array.IndexOf(topRightKeys, topRightKey);
|
||||
if (topRightIndex < 0)
|
||||
topRightIndex = topRightKeys.Length - 1;
|
||||
|
||||
int newTopRightIndex = EditorGUILayout.Popup("Top-Right Key", topRightIndex, topRightKeys);
|
||||
if (newTopRightIndex != topRightIndex)
|
||||
topRightKey = topRightKeys[newTopRightIndex];
|
||||
|
||||
EditorGUILayout.LabelField("Tiles in selection", CountSelectedTiles().ToString());
|
||||
}
|
||||
|
||||
private List<TileKeyOption> GetTopRightOptions(TileKeyOption bottomLeft)
|
||||
{
|
||||
var options = new List<TileKeyOption>();
|
||||
for (int i = 0; i < tileKeyOptions.Count; i++)
|
||||
{
|
||||
var option = tileKeyOptions[i];
|
||||
if (option.X >= bottomLeft.X && option.Y >= bottomLeft.Y)
|
||||
options.Add(option);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private int CountSelectedTiles()
|
||||
{
|
||||
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
|
||||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < tileIndexCache.Count; i++)
|
||||
{
|
||||
var tile = tileIndexCache[i];
|
||||
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
|
||||
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private List<TileRecord> ApplySelection(List<TileRecord> tiles)
|
||||
{
|
||||
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
|
||||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
|
||||
return tiles;
|
||||
|
||||
var selected = new List<TileRecord>();
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var tile = tiles[i];
|
||||
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
|
||||
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
|
||||
selected.Add(tile);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private List<TileRecord> ParseTilesCsv()
|
||||
{
|
||||
var tiles = new List<TileRecord>();
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
var lines = File.ReadAllLines(tilesCsvPath);
|
||||
|
||||
if (lines.Length < 2)
|
||||
{
|
||||
Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row).");
|
||||
return tiles;
|
||||
}
|
||||
|
||||
var headerLine = lines[0].Trim();
|
||||
var headerMap = BuildHeaderMap(headerLine);
|
||||
Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}");
|
||||
|
||||
string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" };
|
||||
if (!HasAll(headerMap, required))
|
||||
{
|
||||
Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " +
|
||||
string.Join(", ", required) +
|
||||
"\nFound: " + string.Join(", ", headerMap.Keys));
|
||||
return tiles;
|
||||
}
|
||||
|
||||
int IDX_TILE = headerMap["tile_id"];
|
||||
int IDX_XMIN = headerMap["xmin"];
|
||||
int IDX_YMIN = headerMap["ymin"];
|
||||
int IDX_GMIN = headerMap["global_min"];
|
||||
int IDX_GMAX = headerMap["global_max"];
|
||||
int IDX_RES = headerMap["out_res"];
|
||||
int IDX_TMIN = headerMap.ContainsKey("tile_min") ? headerMap["tile_min"] : -1;
|
||||
int IDX_TMAX = headerMap.ContainsKey("tile_max") ? headerMap["tile_max"] : -1;
|
||||
int IDX_TILE_KEY = headerMap.ContainsKey("tile_key") ? headerMap["tile_key"] : -1;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
|
||||
if (IDX_TILE_KEY >= 0)
|
||||
needMaxIndex = Math.Max(needMaxIndex, IDX_TILE_KEY);
|
||||
if (IDX_TMIN >= 0)
|
||||
needMaxIndex = Math.Max(needMaxIndex, IDX_TMIN);
|
||||
if (IDX_TMAX >= 0)
|
||||
needMaxIndex = Math.Max(needMaxIndex, IDX_TMAX);
|
||||
|
||||
if (parts.Length <= needMaxIndex)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}).");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var xmin = double.Parse(parts[IDX_XMIN], ci);
|
||||
var ymin = double.Parse(parts[IDX_YMIN], ci);
|
||||
var tileKeyRaw = IDX_TILE_KEY >= 0 ? parts[IDX_TILE_KEY].Trim() : "";
|
||||
var tileKey = ResolveTileKey(tileKeyRaw, xmin, ymin, out var xKey, out var yKey);
|
||||
var globalMin = double.Parse(parts[IDX_GMIN], ci);
|
||||
var globalMax = double.Parse(parts[IDX_GMAX], ci);
|
||||
double tileMin = globalMin;
|
||||
double tileMax = globalMax;
|
||||
if (IDX_TMIN >= 0 && IDX_TMIN < parts.Length &&
|
||||
double.TryParse(parts[IDX_TMIN], NumberStyles.Float, ci, out var parsedTileMin))
|
||||
tileMin = parsedTileMin;
|
||||
if (IDX_TMAX >= 0 && IDX_TMAX < parts.Length &&
|
||||
double.TryParse(parts[IDX_TMAX], NumberStyles.Float, ci, out var parsedTileMax))
|
||||
tileMax = parsedTileMax;
|
||||
|
||||
tiles.Add(new TileRecord
|
||||
{
|
||||
TileId = parts[IDX_TILE].Trim(),
|
||||
TileKey = tileKey,
|
||||
XKey = xKey,
|
||||
YKey = yKey,
|
||||
Xmin = xmin,
|
||||
Ymin = ymin,
|
||||
GlobalMin = globalMin,
|
||||
GlobalMax = globalMax,
|
||||
TileMin = tileMin,
|
||||
TileMax = tileMax,
|
||||
OutRes = int.Parse(parts[IDX_RES], ci)
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTileImporter] Parse error line {i + 1}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, out int xKey, out int yKey)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey))
|
||||
return tileKeyRaw;
|
||||
|
||||
float sizeX = tileKeySizeX <= 0f ? 1f : tileKeySizeX;
|
||||
float sizeY = tileKeySizeY <= 0f ? 1f : tileKeySizeY;
|
||||
xKey = (int)Math.Floor((xmin + tileKeyOverlapX) / sizeX);
|
||||
yKey = (int)Math.Floor((ymin + tileKeyOverlapY) / sizeY);
|
||||
return $"{xKey}_{yKey}";
|
||||
}
|
||||
|
||||
private static bool TryParseTileKey(string tileKey, out int xKey, out int yKey)
|
||||
{
|
||||
xKey = 0;
|
||||
yKey = 0;
|
||||
if (string.IsNullOrWhiteSpace(tileKey))
|
||||
return false;
|
||||
|
||||
var parts = tileKey.Split('_');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
return int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out xKey)
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out yKey);
|
||||
}
|
||||
|
||||
private static bool ContainsKey(List<TileKeyOption> options, string key)
|
||||
{
|
||||
for (int i = 0; i < options.Count; i++)
|
||||
{
|
||||
if (string.Equals(options[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] BuildKeyArray(List<TileKeyOption> options)
|
||||
{
|
||||
var keys = new string[options.Count];
|
||||
for (int i = 0; i < options.Count; i++)
|
||||
keys[i] = options[i].Key;
|
||||
return keys;
|
||||
}
|
||||
|
||||
private struct TileRecord
|
||||
{
|
||||
public string TileId;
|
||||
public string TileKey;
|
||||
public int XKey;
|
||||
public int YKey;
|
||||
public double Xmin;
|
||||
public double Ymin;
|
||||
public double GlobalMin;
|
||||
public double GlobalMax;
|
||||
public double TileMin;
|
||||
public double TileMax;
|
||||
public int OutRes;
|
||||
}
|
||||
|
||||
private struct TileKeyOption
|
||||
{
|
||||
public string Key;
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
private static bool TryGetTileXY(string tileId, out int x, out int y)
|
||||
{
|
||||
x = 0;
|
||||
y = 0;
|
||||
if (string.IsNullOrWhiteSpace(tileId))
|
||||
return false;
|
||||
|
||||
var parts = tileId.Split('_');
|
||||
var coords = new List<int>();
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
string part = parts[i];
|
||||
if (part.Length < 3)
|
||||
continue;
|
||||
|
||||
bool allDigits = true;
|
||||
for (int j = 0; j < part.Length; j++)
|
||||
{
|
||||
if (!char.IsDigit(part[j]))
|
||||
{
|
||||
allDigits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allDigits)
|
||||
continue;
|
||||
|
||||
if (int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
|
||||
coords.Add(value);
|
||||
}
|
||||
|
||||
if (coords.Count < 2)
|
||||
return false;
|
||||
|
||||
x = coords[coords.Count - 2];
|
||||
y = coords[coords.Count - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ShouldImportBuildingsForTile(string tileId, bool evenTilesOnly)
|
||||
{
|
||||
if (!evenTilesOnly)
|
||||
return true;
|
||||
|
||||
if (!TryGetTileXY(tileId, out int x, out int y))
|
||||
return true;
|
||||
|
||||
return (x % 2 == 0) && (y % 2 == 0);
|
||||
}
|
||||
|
||||
private void ImportBuildings(List<(string tileId, float ux, float uz, float baseY)> placements)
|
||||
{
|
||||
if (!importBuildings)
|
||||
return;
|
||||
@@ -567,9 +894,15 @@ public class GeoTileImporter : EditorWindow
|
||||
DestroyImmediate(parent.transform.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
int imported = 0, missing = 0;
|
||||
foreach (var (tileId, ux, uz, baseMin) in placements)
|
||||
int imported = 0, missing = 0, skipped = 0;
|
||||
foreach (var (tileId, ux, uz, baseY) in placements)
|
||||
{
|
||||
if (!ShouldImportBuildingsForTile(tileId, importBuildingsEvenTilesOnly))
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string glbPath = Path.Combine(activeDir, $"{tileId}.glb").Replace("\\", "/");
|
||||
if (!File.Exists(glbPath))
|
||||
{
|
||||
@@ -586,19 +919,25 @@ public class GeoTileImporter : EditorWindow
|
||||
continue;
|
||||
}
|
||||
|
||||
var tileContainer = new GameObject($"Buildings_{tileId}");
|
||||
tileContainer.transform.SetParent(parent.transform, false);
|
||||
tileContainer.transform.position = new Vector3(ux, baseY, uz);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
var inst = PrefabUtility.InstantiatePrefab(prefab) as GameObject ?? Instantiate(prefab);
|
||||
inst.name = tileId;
|
||||
inst.transform.SetParent(parent.transform, false);
|
||||
inst.transform.position = new Vector3(ux, baseMin, uz);
|
||||
inst.transform.SetParent(tileContainer.transform, false);
|
||||
// GLB vertices store absolute elevation; offset by -baseY to keep world Y absolute.
|
||||
inst.transform.localPosition = new Vector3(0f, -baseY, 0f);
|
||||
inst.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
inst.isStatic = true;
|
||||
imported++;
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTileImporter] Buildings ({sourceLabel}) imported={imported}, missing/failed={missing} under '{buildingsParentName}'.");
|
||||
Debug.Log($"[GeoTileImporter] Buildings ({sourceLabel}) imported={imported}, skipped={skipped}, missing/failed={missing} under '{buildingsParentName}'.");
|
||||
}
|
||||
|
||||
private void ImportTrees(List<(string tileId, float ux, float uz, float baseMin)> placements)
|
||||
private void ImportTrees(List<(string tileId, float ux, float uz, float baseY)> placements)
|
||||
{
|
||||
if (!importTrees)
|
||||
return;
|
||||
@@ -623,7 +962,7 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
|
||||
int importedTiles = 0, importedChunks = 0, missingTiles = 0;
|
||||
foreach (var (tileId, ux, uz, baseMin) in placements)
|
||||
foreach (var (tileId, ux, uz, baseY) in placements)
|
||||
{
|
||||
// Look for chunk files: {tileId}_0_0.glb, {tileId}_0_1.glb, etc.
|
||||
// Standard tree export creates 4x4 chunks per tile
|
||||
@@ -647,7 +986,7 @@ public class GeoTileImporter : EditorWindow
|
||||
// Create container for this tile's tree chunks
|
||||
var tileContainer = new GameObject($"Trees_{tileId}");
|
||||
tileContainer.transform.SetParent(parent.transform, false);
|
||||
tileContainer.transform.position = new Vector3(ux, baseMin, uz);
|
||||
tileContainer.transform.position = new Vector3(ux, baseY, uz);
|
||||
tileContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
@@ -664,7 +1003,8 @@ public class GeoTileImporter : EditorWindow
|
||||
var inst = PrefabUtility.InstantiatePrefab(prefab) as GameObject ?? Instantiate(prefab);
|
||||
inst.name = Path.GetFileNameWithoutExtension(chunkPath);
|
||||
inst.transform.SetParent(tileContainer.transform, false);
|
||||
inst.transform.localPosition = Vector3.zero; // Chunks already have correct local positions
|
||||
// GLB vertices store absolute elevation; offset by -baseY to keep world Y absolute.
|
||||
inst.transform.localPosition = new Vector3(0f, -baseY, 0f);
|
||||
inst.isStatic = true;
|
||||
importedChunks++;
|
||||
}
|
||||
@@ -675,7 +1015,7 @@ public class GeoTileImporter : EditorWindow
|
||||
Debug.Log($"[GeoTileImporter] Trees imported: {importedTiles} tiles, {importedChunks} chunks, {missingTiles} missing under '{treesParentName}'.");
|
||||
}
|
||||
|
||||
private void ImportFurniture(List<(string tileId, float ux, float uz, float baseMin)> placements)
|
||||
private void ImportFurniture(List<(string tileId, float ux, float uz, float baseY)> placements)
|
||||
{
|
||||
if (!importFurniture)
|
||||
return;
|
||||
@@ -697,7 +1037,7 @@ public class GeoTileImporter : EditorWindow
|
||||
int imported = 0, skipped = 0;
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
|
||||
foreach (var (tileId, ux, uz, baseMin) in placements)
|
||||
foreach (var (tileId, ux, uz, baseY) in placements)
|
||||
{
|
||||
string csvPath = Path.Combine(furnitureDir, $"{tileId}.csv").Replace("\\", "/");
|
||||
if (!File.Exists(csvPath))
|
||||
@@ -731,7 +1071,7 @@ public class GeoTileImporter : EditorWindow
|
||||
// Create tile container
|
||||
var tileContainer = new GameObject($"Furniture_{tileId}");
|
||||
tileContainer.transform.SetParent(parent.transform, false);
|
||||
tileContainer.transform.position = new Vector3(ux, baseMin, uz);
|
||||
tileContainer.transform.position = new Vector3(ux, baseY, uz);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
@@ -802,7 +1142,7 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
|
||||
obj.transform.SetParent(tileContainer.transform, false);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
|
||||
obj.isStatic = true;
|
||||
imported++;
|
||||
}
|
||||
@@ -817,7 +1157,7 @@ public class GeoTileImporter : EditorWindow
|
||||
Debug.Log($"[GeoTileImporter] Furniture imported={imported}, skipped={skipped} under '{furnitureParentName}'.");
|
||||
}
|
||||
|
||||
private void ImportEnhancedTrees(List<(string tileId, float ux, float uz, float baseMin)> placements)
|
||||
private void ImportEnhancedTrees(List<(string tileId, float ux, float uz, float baseY)> placements)
|
||||
{
|
||||
if (!importEnhancedTrees)
|
||||
return;
|
||||
@@ -839,7 +1179,7 @@ public class GeoTileImporter : EditorWindow
|
||||
int imported = 0, skipped = 0;
|
||||
var ci = CultureInfo.InvariantCulture;
|
||||
|
||||
foreach (var (tileId, ux, uz, baseMin) in placements)
|
||||
foreach (var (tileId, ux, uz, baseY) in placements)
|
||||
{
|
||||
string csvPath = Path.Combine(enhancedTreesDir, $"{tileId}.csv").Replace("\\", "/");
|
||||
if (!File.Exists(csvPath))
|
||||
@@ -877,7 +1217,7 @@ public class GeoTileImporter : EditorWindow
|
||||
// Create tile container
|
||||
var tileContainer = new GameObject($"Trees_{tileId}");
|
||||
tileContainer.transform.SetParent(parent.transform, false);
|
||||
tileContainer.transform.position = new Vector3(ux, baseMin, uz);
|
||||
tileContainer.transform.position = new Vector3(ux, baseY, uz);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
@@ -975,7 +1315,7 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
|
||||
treeObj.transform.SetParent(tileContainer.transform, false);
|
||||
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
|
||||
treeObj.isStatic = true;
|
||||
imported++;
|
||||
}
|
||||
|
||||
@@ -15,14 +15,18 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
private string tilesCsvPath = "Assets/GeoData/tile_index.csv";
|
||||
private string heightmapsDir = "Assets/GeoData/height_png16";
|
||||
private string orthoDir = "Assets/GeoData/ortho_jpg";
|
||||
private string orthoDirFallback = "Assets/GeoData/ortho_jpg_river";
|
||||
private string buildingsDir = "Assets/GeoData/buildings_tiles";
|
||||
private string treesDir = "Assets/GeoData/trees_tiles";
|
||||
private string furnitureDir = "Assets/GeoData/street_furniture";
|
||||
private string enhancedTreesDir = "Assets/GeoData/trees_enhanced";
|
||||
|
||||
// Output settings
|
||||
private string prefabOutputDir = "Assets/GeoData/TilePrefabs";
|
||||
private string prefabOutputDir = "Assets/TilePrefabs";
|
||||
private bool overwriteExisting = false;
|
||||
private string buildingPrefabsDir = "Assets/TilePrefabs_Buildings";
|
||||
private bool exportBuildingPrefabs = true;
|
||||
private bool overwriteBuildingPrefabs = false;
|
||||
|
||||
// Terrain settings
|
||||
private float tileSizeMeters = 1000f;
|
||||
@@ -31,10 +35,29 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
// Component toggles
|
||||
private bool applyOrthoTextures = true;
|
||||
private bool includeBuildings = true;
|
||||
private bool includeBuildingsEvenTilesOnly = true;
|
||||
private bool includeTrees = true;
|
||||
private bool includeFurniture = false;
|
||||
private bool includeEnhancedTrees = false;
|
||||
|
||||
private Vector2 scrollPosition;
|
||||
private float tileKeySizeX = 1000f;
|
||||
private float tileKeySizeY = 1000f;
|
||||
private float tileKeyOverlapX = 0.5f;
|
||||
private float tileKeyOverlapY = 0.5f;
|
||||
private string bottomLeftKey = "";
|
||||
private string topRightKey = "";
|
||||
private readonly List<TileMetadata> tileIndexCache = new List<TileMetadata>();
|
||||
private readonly List<TileKeyOption> tileKeyOptions = new List<TileKeyOption>();
|
||||
private readonly Dictionary<string, TileKeyOption> tileKeyLookup = new Dictionary<string, TileKeyOption>(StringComparer.OrdinalIgnoreCase);
|
||||
private string[] tileKeyStrings = Array.Empty<string>();
|
||||
private string cachedTileCsvPath = "";
|
||||
private DateTime cachedTileCsvTimeUtc = DateTime.MinValue;
|
||||
private float cachedTileKeySizeX = 0f;
|
||||
private float cachedTileKeySizeY = 0f;
|
||||
private float cachedTileOverlapX = 0f;
|
||||
private float cachedTileOverlapY = 0f;
|
||||
|
||||
// Prefabs for furniture (optional)
|
||||
private GameObject lampPrefab;
|
||||
private GameObject benchPrefab;
|
||||
@@ -45,11 +68,13 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
private struct TileMetadata
|
||||
{
|
||||
public string TileKey;
|
||||
public string TileId;
|
||||
public int XKey;
|
||||
public int YKey;
|
||||
public double Xmin, Ymin, Xmax, Ymax;
|
||||
public double GlobalMin, GlobalMax;
|
||||
public double TileMin, TileMax;
|
||||
public bool HasTileMinMax;
|
||||
public int OutRes;
|
||||
}
|
||||
|
||||
@@ -62,10 +87,13 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
GUILayout.Label("Input Paths", EditorStyles.boldLabel);
|
||||
tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath);
|
||||
heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir);
|
||||
orthoDir = EditorGUILayout.TextField("ortho_jpg dir", orthoDir);
|
||||
orthoDir = EditorGUILayout.TextField("ortho_jpg dir (primary)", orthoDir);
|
||||
orthoDirFallback = EditorGUILayout.TextField("ortho_jpg dir (fallback)", orthoDirFallback);
|
||||
buildingsDir = EditorGUILayout.TextField("buildings_tiles dir", buildingsDir);
|
||||
treesDir = EditorGUILayout.TextField("trees_tiles dir", treesDir);
|
||||
furnitureDir = EditorGUILayout.TextField("street_furniture dir", furnitureDir);
|
||||
@@ -75,6 +103,9 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
GUILayout.Label("Output Settings", EditorStyles.boldLabel);
|
||||
prefabOutputDir = EditorGUILayout.TextField("Prefab output dir", prefabOutputDir);
|
||||
overwriteExisting = EditorGUILayout.ToggleLeft("Overwrite existing prefabs", overwriteExisting);
|
||||
buildingPrefabsDir = EditorGUILayout.TextField("Building prefab output dir", buildingPrefabsDir);
|
||||
exportBuildingPrefabs = EditorGUILayout.ToggleLeft("Export building-only prefabs (2km blocks)", exportBuildingPrefabs);
|
||||
overwriteBuildingPrefabs = EditorGUILayout.ToggleLeft("Overwrite building prefabs", overwriteBuildingPrefabs);
|
||||
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Terrain Settings", EditorStyles.boldLabel);
|
||||
@@ -85,6 +116,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
GUILayout.Label("Include Components", EditorStyles.boldLabel);
|
||||
applyOrthoTextures = EditorGUILayout.ToggleLeft("Apply ortho textures", applyOrthoTextures);
|
||||
includeBuildings = EditorGUILayout.ToggleLeft("Include buildings (GLB)", includeBuildings);
|
||||
includeBuildingsEvenTilesOnly = EditorGUILayout.ToggleLeft("Include 2km buildings once (even X/Y tiles only)", includeBuildingsEvenTilesOnly);
|
||||
includeTrees = EditorGUILayout.ToggleLeft("Include trees (GLB chunks)", includeTrees);
|
||||
includeFurniture = EditorGUILayout.ToggleLeft("Include street furniture (CSV)", includeFurniture);
|
||||
includeEnhancedTrees = EditorGUILayout.ToggleLeft("Include enhanced trees (CSV)", includeEnhancedTrees);
|
||||
@@ -98,6 +130,19 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
|
||||
defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture", defaultFurniturePrefab, typeof(GameObject), false);
|
||||
|
||||
GUILayout.Space(12);
|
||||
GUILayout.Label("Tile Key Config", EditorStyles.boldLabel);
|
||||
tileKeySizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileKeySizeX);
|
||||
tileKeySizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileKeySizeY);
|
||||
tileKeyOverlapX = EditorGUILayout.FloatField("Overlap X (m)", tileKeyOverlapX);
|
||||
tileKeyOverlapY = EditorGUILayout.FloatField("Overlap Y (m)", tileKeyOverlapY);
|
||||
|
||||
RefreshTileIndexCache();
|
||||
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Tile Selection", EditorStyles.boldLabel);
|
||||
DrawTileSelectionUI();
|
||||
|
||||
GUILayout.Space(15);
|
||||
if (GUILayout.Button("Generate Prefabs"))
|
||||
ImportTilesAsPrefabs();
|
||||
@@ -110,8 +155,11 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
"Creates one .prefab asset per tile in the manifest.\n" +
|
||||
"Each prefab contains: Terrain (with TerrainData), Buildings, Trees, Furniture.\n" +
|
||||
"TerrainData and TerrainLayers are saved as separate .asset files.\n" +
|
||||
"Per-tile elevation: uses tile_min/tile_max when present (falls back to global_min/global_max).\n" +
|
||||
"IMPORTANT: Use 'Place All Prefabs in Scene' to position tiles correctly.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void PlaceAllPrefabsInScene()
|
||||
@@ -211,8 +259,11 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
EnsureDirectoryExists(prefabOutputDir);
|
||||
EnsureDirectoryExists($"{prefabOutputDir}/TerrainData");
|
||||
EnsureDirectoryExists($"{prefabOutputDir}/TerrainLayers");
|
||||
if (exportBuildingPrefabs)
|
||||
EnsureDirectoryExists(buildingPrefabsDir);
|
||||
|
||||
// Parse CSV
|
||||
RefreshTileIndexCache();
|
||||
var tiles = ParseTilesCsv();
|
||||
if (tiles == null || tiles.Count == 0)
|
||||
{
|
||||
@@ -220,32 +271,54 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTilePrefabImporter] Found {tiles.Count} tiles to process.");
|
||||
var selectedTiles = ApplySelection(tiles);
|
||||
if (selectedTiles.Count == 0)
|
||||
{
|
||||
Debug.LogError("[GeoTilePrefabImporter] Selection is empty. Adjust Tile Selection and try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTilePrefabImporter] Found {selectedTiles.Count} tiles to process.");
|
||||
|
||||
int created = 0, skipped = 0, failed = 0;
|
||||
int buildingsCreated = 0, buildingsSkipped = 0, buildingsFailed = 0;
|
||||
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
for (int i = 0; i < selectedTiles.Count; i++)
|
||||
{
|
||||
var tile = tiles[i];
|
||||
var tile = selectedTiles[i];
|
||||
EditorUtility.DisplayProgressBar(
|
||||
"Creating Tile Prefabs",
|
||||
$"Processing {tile.TileId} ({i + 1}/{tiles.Count})",
|
||||
(float)i / tiles.Count);
|
||||
$"Processing {tile.TileId} ({i + 1}/{selectedTiles.Count})",
|
||||
(float)i / selectedTiles.Count);
|
||||
|
||||
string prefabPath = $"{prefabOutputDir}/{tile.TileId}.prefab";
|
||||
if (File.Exists(prefabPath) && !overwriteExisting)
|
||||
bool skipTerrain = File.Exists(prefabPath) && !overwriteExisting;
|
||||
if (skipTerrain)
|
||||
{
|
||||
Debug.Log($"[GeoTilePrefabImporter] Skipping existing: {tile.TileId}");
|
||||
Debug.Log($"[GeoTilePrefabImporter] Skipping existing terrain prefab: {tile.TileId}");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (CreateTilePrefab(tile))
|
||||
created++;
|
||||
else
|
||||
failed++;
|
||||
if (!skipTerrain)
|
||||
{
|
||||
if (CreateTilePrefab(tile))
|
||||
created++;
|
||||
else
|
||||
failed++;
|
||||
}
|
||||
|
||||
if (exportBuildingPrefabs)
|
||||
{
|
||||
var result = CreateBuildingPrefab(tile);
|
||||
if (result == BuildResult.Created)
|
||||
buildingsCreated++;
|
||||
else if (result == BuildResult.Skipped)
|
||||
buildingsSkipped++;
|
||||
else
|
||||
buildingsFailed++;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -258,7 +331,170 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"[GeoTilePrefabImporter] DONE. Created={created}, Skipped={skipped}, Failed={failed}");
|
||||
Debug.Log($"[GeoTilePrefabImporter] DONE. Created={created}, Skipped={skipped}, Failed={failed}" +
|
||||
(exportBuildingPrefabs ? $", Buildings Created={buildingsCreated}, Skipped={buildingsSkipped}, Failed={buildingsFailed}" : ""));
|
||||
}
|
||||
|
||||
private void RefreshTileIndexCache()
|
||||
{
|
||||
if (!File.Exists(tilesCsvPath))
|
||||
{
|
||||
tileIndexCache.Clear();
|
||||
tileKeyOptions.Clear();
|
||||
tileKeyLookup.Clear();
|
||||
tileKeyStrings = Array.Empty<string>();
|
||||
return;
|
||||
}
|
||||
|
||||
var writeTime = File.GetLastWriteTimeUtc(tilesCsvPath);
|
||||
if (tilesCsvPath == cachedTileCsvPath &&
|
||||
writeTime == cachedTileCsvTimeUtc &&
|
||||
Mathf.Approximately(tileKeySizeX, cachedTileKeySizeX) &&
|
||||
Mathf.Approximately(tileKeySizeY, cachedTileKeySizeY) &&
|
||||
Mathf.Approximately(tileKeyOverlapX, cachedTileOverlapX) &&
|
||||
Mathf.Approximately(tileKeyOverlapY, cachedTileOverlapY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tileIndexCache.Clear();
|
||||
tileIndexCache.AddRange(ParseTilesCsv());
|
||||
BuildTileKeyOptions();
|
||||
|
||||
cachedTileCsvPath = tilesCsvPath;
|
||||
cachedTileCsvTimeUtc = writeTime;
|
||||
cachedTileKeySizeX = tileKeySizeX;
|
||||
cachedTileKeySizeY = tileKeySizeY;
|
||||
cachedTileOverlapX = tileKeyOverlapX;
|
||||
cachedTileOverlapY = tileKeyOverlapY;
|
||||
}
|
||||
|
||||
private void BuildTileKeyOptions()
|
||||
{
|
||||
tileKeyOptions.Clear();
|
||||
tileKeyLookup.Clear();
|
||||
|
||||
for (int i = 0; i < tileIndexCache.Count; i++)
|
||||
{
|
||||
var tile = tileIndexCache[i];
|
||||
if (string.IsNullOrWhiteSpace(tile.TileKey))
|
||||
continue;
|
||||
|
||||
if (tileKeyLookup.ContainsKey(tile.TileKey))
|
||||
continue;
|
||||
|
||||
var option = new TileKeyOption
|
||||
{
|
||||
Key = tile.TileKey,
|
||||
X = tile.XKey,
|
||||
Y = tile.YKey
|
||||
};
|
||||
|
||||
tileKeyOptions.Add(option);
|
||||
tileKeyLookup[tile.TileKey] = option;
|
||||
}
|
||||
|
||||
tileKeyOptions.Sort((a, b) =>
|
||||
{
|
||||
int cmp = a.X.CompareTo(b.X);
|
||||
return cmp != 0 ? cmp : a.Y.CompareTo(b.Y);
|
||||
});
|
||||
|
||||
tileKeyStrings = new string[tileKeyOptions.Count];
|
||||
for (int i = 0; i < tileKeyOptions.Count; i++)
|
||||
tileKeyStrings[i] = tileKeyOptions[i].Key;
|
||||
}
|
||||
|
||||
private void DrawTileSelectionUI()
|
||||
{
|
||||
if (tileKeyOptions.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("No tile keys available. Check the tile index CSV and key config.", MessageType.Info);
|
||||
EditorGUILayout.LabelField("Tiles in selection", "0");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(bottomLeftKey) || !tileKeyLookup.ContainsKey(bottomLeftKey))
|
||||
bottomLeftKey = tileKeyOptions[0].Key;
|
||||
|
||||
var bottomLeft = tileKeyLookup[bottomLeftKey];
|
||||
var topRightOptions = GetTopRightOptions(bottomLeft);
|
||||
if (topRightOptions.Count == 0)
|
||||
topRightOptions.Add(bottomLeft);
|
||||
|
||||
if (string.IsNullOrEmpty(topRightKey) || !ContainsKey(topRightOptions, topRightKey))
|
||||
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
|
||||
|
||||
int bottomLeftIndex = Array.IndexOf(tileKeyStrings, bottomLeftKey);
|
||||
int newBottomLeftIndex = EditorGUILayout.Popup("Bottom-Left Key", bottomLeftIndex, tileKeyStrings);
|
||||
if (newBottomLeftIndex != bottomLeftIndex)
|
||||
{
|
||||
bottomLeftKey = tileKeyStrings[newBottomLeftIndex];
|
||||
bottomLeft = tileKeyLookup[bottomLeftKey];
|
||||
topRightOptions = GetTopRightOptions(bottomLeft);
|
||||
if (topRightOptions.Count == 0)
|
||||
topRightOptions.Add(bottomLeft);
|
||||
|
||||
if (!ContainsKey(topRightOptions, topRightKey))
|
||||
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
|
||||
}
|
||||
|
||||
var topRightKeys = BuildKeyArray(topRightOptions);
|
||||
int topRightIndex = Array.IndexOf(topRightKeys, topRightKey);
|
||||
if (topRightIndex < 0)
|
||||
topRightIndex = topRightKeys.Length - 1;
|
||||
|
||||
int newTopRightIndex = EditorGUILayout.Popup("Top-Right Key", topRightIndex, topRightKeys);
|
||||
if (newTopRightIndex != topRightIndex)
|
||||
topRightKey = topRightKeys[newTopRightIndex];
|
||||
|
||||
EditorGUILayout.LabelField("Tiles in selection", CountSelectedTiles().ToString());
|
||||
}
|
||||
|
||||
private List<TileKeyOption> GetTopRightOptions(TileKeyOption bottomLeft)
|
||||
{
|
||||
var options = new List<TileKeyOption>();
|
||||
for (int i = 0; i < tileKeyOptions.Count; i++)
|
||||
{
|
||||
var option = tileKeyOptions[i];
|
||||
if (option.X >= bottomLeft.X && option.Y >= bottomLeft.Y)
|
||||
options.Add(option);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private int CountSelectedTiles()
|
||||
{
|
||||
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
|
||||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < tileIndexCache.Count; i++)
|
||||
{
|
||||
var tile = tileIndexCache[i];
|
||||
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
|
||||
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private List<TileMetadata> ApplySelection(List<TileMetadata> tiles)
|
||||
{
|
||||
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
|
||||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
|
||||
return tiles;
|
||||
|
||||
var selected = new List<TileMetadata>();
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
{
|
||||
var tile = tiles[i];
|
||||
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
|
||||
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
|
||||
selected.Add(tile);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private List<TileMetadata> ParseTilesCsv()
|
||||
@@ -289,10 +525,10 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
int IDX_YMAX = headerMap["ymax"];
|
||||
int IDX_GMIN = headerMap["global_min"];
|
||||
int IDX_GMAX = headerMap["global_max"];
|
||||
int IDX_TMIN = headerMap.ContainsKey("tile_min") ? headerMap["tile_min"] : -1;
|
||||
int IDX_TMAX = headerMap.ContainsKey("tile_max") ? headerMap["tile_max"] : -1;
|
||||
int IDX_RES = headerMap["out_res"];
|
||||
bool hasTileMin = headerMap.TryGetValue("tile_min", out int IDX_TMIN);
|
||||
bool hasTileMax = headerMap.TryGetValue("tile_max", out int IDX_TMAX);
|
||||
bool useTileRange = hasTileMin && hasTileMax;
|
||||
int IDX_TILE_KEY = headerMap.ContainsKey("tile_key") ? headerMap["tile_key"] : -1;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -300,9 +536,15 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
int maxIdx = Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMAX), IDX_YMAX), Math.Max(IDX_GMAX, IDX_RES));
|
||||
if (useTileRange)
|
||||
maxIdx = Math.Max(maxIdx, Math.Max(IDX_TMIN, IDX_TMAX));
|
||||
int maxIdx = Math.Max(
|
||||
Math.Max(Math.Max(IDX_TILE, IDX_XMIN), Math.Max(IDX_YMIN, IDX_XMAX)),
|
||||
Math.Max(Math.Max(IDX_YMAX, IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES)));
|
||||
if (IDX_TILE_KEY >= 0)
|
||||
maxIdx = Math.Max(maxIdx, IDX_TILE_KEY);
|
||||
if (IDX_TMIN >= 0)
|
||||
maxIdx = Math.Max(maxIdx, IDX_TMIN);
|
||||
if (IDX_TMAX >= 0)
|
||||
maxIdx = Math.Max(maxIdx, IDX_TMAX);
|
||||
if (parts.Length <= maxIdx)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Skipping line {i + 1}: too few columns.");
|
||||
@@ -311,38 +553,35 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
try
|
||||
{
|
||||
double gmin = double.Parse(parts[IDX_GMIN], ci);
|
||||
double gmax = double.Parse(parts[IDX_GMAX], ci);
|
||||
double tileMin = gmin;
|
||||
double tileMax = gmax;
|
||||
bool tileRangeValid = false;
|
||||
if (useTileRange)
|
||||
{
|
||||
if (double.TryParse(parts[IDX_TMIN], NumberStyles.Float, ci, out double tmin) &&
|
||||
double.TryParse(parts[IDX_TMAX], NumberStyles.Float, ci, out double tmax))
|
||||
{
|
||||
tileMin = tmin;
|
||||
tileMax = tmax;
|
||||
tileRangeValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {parts[IDX_TILE].Trim()}: invalid tile_min/tile_max; falling back to global range.");
|
||||
}
|
||||
}
|
||||
var xmin = double.Parse(parts[IDX_XMIN], ci);
|
||||
var ymin = double.Parse(parts[IDX_YMIN], ci);
|
||||
var tileKeyRaw = IDX_TILE_KEY >= 0 ? parts[IDX_TILE_KEY].Trim() : "";
|
||||
var tileKey = ResolveTileKey(tileKeyRaw, xmin, ymin, out var xKey, out var yKey);
|
||||
var globalMin = double.Parse(parts[IDX_GMIN], ci);
|
||||
var globalMax = double.Parse(parts[IDX_GMAX], ci);
|
||||
double tileMin = globalMin;
|
||||
double tileMax = globalMax;
|
||||
if (IDX_TMIN >= 0 && IDX_TMIN < parts.Length &&
|
||||
double.TryParse(parts[IDX_TMIN], NumberStyles.Float, ci, out var parsedTileMin))
|
||||
tileMin = parsedTileMin;
|
||||
if (IDX_TMAX >= 0 && IDX_TMAX < parts.Length &&
|
||||
double.TryParse(parts[IDX_TMAX], NumberStyles.Float, ci, out var parsedTileMax))
|
||||
tileMax = parsedTileMax;
|
||||
|
||||
tiles.Add(new TileMetadata
|
||||
{
|
||||
TileKey = tileKey,
|
||||
TileId = parts[IDX_TILE].Trim(),
|
||||
Xmin = double.Parse(parts[IDX_XMIN], ci),
|
||||
Ymin = double.Parse(parts[IDX_YMIN], ci),
|
||||
XKey = xKey,
|
||||
YKey = yKey,
|
||||
Xmin = xmin,
|
||||
Ymin = ymin,
|
||||
Xmax = double.Parse(parts[IDX_XMAX], ci),
|
||||
Ymax = double.Parse(parts[IDX_YMAX], ci),
|
||||
GlobalMin = gmin,
|
||||
GlobalMax = gmax,
|
||||
GlobalMin = globalMin,
|
||||
GlobalMax = globalMax,
|
||||
TileMin = tileMin,
|
||||
TileMax = tileMax,
|
||||
HasTileMinMax = tileRangeValid,
|
||||
OutRes = int.Parse(parts[IDX_RES], ci)
|
||||
});
|
||||
}
|
||||
@@ -355,24 +594,81 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, out int xKey, out int yKey)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey))
|
||||
return tileKeyRaw;
|
||||
|
||||
float sizeX = tileKeySizeX <= 0f ? 1f : tileKeySizeX;
|
||||
float sizeY = tileKeySizeY <= 0f ? 1f : tileKeySizeY;
|
||||
xKey = (int)Math.Floor((xmin + tileKeyOverlapX) / sizeX);
|
||||
yKey = (int)Math.Floor((ymin + tileKeyOverlapY) / sizeY);
|
||||
return $"{xKey}_{yKey}";
|
||||
}
|
||||
|
||||
private static bool TryParseTileKey(string tileKey, out int xKey, out int yKey)
|
||||
{
|
||||
xKey = 0;
|
||||
yKey = 0;
|
||||
if (string.IsNullOrWhiteSpace(tileKey))
|
||||
return false;
|
||||
|
||||
var parts = tileKey.Split('_');
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
return int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out xKey)
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out yKey);
|
||||
}
|
||||
|
||||
private static bool ContainsKey(List<TileKeyOption> options, string key)
|
||||
{
|
||||
for (int i = 0; i < options.Count; i++)
|
||||
{
|
||||
if (string.Equals(options[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] BuildKeyArray(List<TileKeyOption> options)
|
||||
{
|
||||
var keys = new string[options.Count];
|
||||
for (int i = 0; i < options.Count; i++)
|
||||
keys[i] = options[i].Key;
|
||||
return keys;
|
||||
}
|
||||
|
||||
private struct TileKeyOption
|
||||
{
|
||||
public string Key;
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
private static bool ShouldIncludeBuildings(TileMetadata tile, bool evenTilesOnly)
|
||||
{
|
||||
if (!evenTilesOnly)
|
||||
return true;
|
||||
|
||||
return (tile.XKey % 2 == 0) && (tile.YKey % 2 == 0);
|
||||
}
|
||||
|
||||
private enum BuildResult
|
||||
{
|
||||
Created,
|
||||
Skipped,
|
||||
Failed
|
||||
}
|
||||
|
||||
private bool CreateTilePrefab(TileMetadata tile)
|
||||
{
|
||||
// Validate height range
|
||||
double baseMin = tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin;
|
||||
double baseMax = tile.HasTileMinMax ? tile.TileMax : tile.GlobalMax;
|
||||
float heightRange = (float)(baseMax - baseMin);
|
||||
float heightRange = (float)(tile.TileMax - tile.TileMin);
|
||||
if (heightRange <= 0.0001f)
|
||||
{
|
||||
if (tile.HasTileMinMax)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {tile.TileId}: flat tile range; using epsilon height range.");
|
||||
heightRange = 0.001f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {tile.TileId}: invalid height range. Skipping.");
|
||||
return false;
|
||||
}
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {tile.TileId}: invalid height range. Skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load heightmap
|
||||
@@ -447,6 +743,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
// Store metadata as component for later use
|
||||
var metadata = root.AddComponent<GeoTileMetadata>();
|
||||
metadata.tileKey = tile.TileKey;
|
||||
metadata.tileId = tile.TileId;
|
||||
metadata.xmin = tile.Xmin;
|
||||
metadata.ymin = tile.Ymin;
|
||||
@@ -454,7 +751,6 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
metadata.globalMax = tile.GlobalMax;
|
||||
metadata.tileMin = tile.TileMin;
|
||||
metadata.tileMax = tile.TileMax;
|
||||
metadata.hasTileMinMax = tile.HasTileMinMax;
|
||||
|
||||
// Add child components
|
||||
if (includeBuildings)
|
||||
@@ -483,9 +779,53 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
return true;
|
||||
}
|
||||
|
||||
private BuildResult CreateBuildingPrefab(TileMetadata tile)
|
||||
{
|
||||
if (!exportBuildingPrefabs)
|
||||
return BuildResult.Skipped;
|
||||
if (!ShouldIncludeBuildings(tile, includeBuildingsEvenTilesOnly))
|
||||
return BuildResult.Skipped;
|
||||
|
||||
string glbPath = Path.Combine(buildingsDir, $"{tile.TileId}.glb").Replace("\\", "/");
|
||||
if (!File.Exists(glbPath))
|
||||
return BuildResult.Skipped;
|
||||
|
||||
string prefabPath = $"{buildingPrefabsDir}/{tile.TileId}.prefab";
|
||||
if (File.Exists(prefabPath))
|
||||
{
|
||||
if (!overwriteBuildingPrefabs)
|
||||
return BuildResult.Skipped;
|
||||
AssetDatabase.DeleteAsset(prefabPath);
|
||||
}
|
||||
|
||||
var root = new GameObject(tile.TileId);
|
||||
var metadata = root.AddComponent<GeoTileMetadata>();
|
||||
metadata.tileKey = tile.TileKey;
|
||||
metadata.tileId = tile.TileId;
|
||||
metadata.xmin = tile.Xmin;
|
||||
metadata.ymin = tile.Ymin;
|
||||
metadata.globalMin = tile.GlobalMin;
|
||||
metadata.globalMax = tile.GlobalMax;
|
||||
metadata.tileMin = tile.TileMin;
|
||||
metadata.tileMax = tile.TileMax;
|
||||
|
||||
AddBuildings(root, tile);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
||||
Debug.Log($"[GeoTilePrefabImporter] Created building prefab: {prefabPath}");
|
||||
DestroyImmediate(root);
|
||||
|
||||
return BuildResult.Created;
|
||||
}
|
||||
|
||||
private void ApplyOrthoTexture(TerrainData terrainData, string tileId)
|
||||
{
|
||||
string orthoPath = Path.Combine(orthoDir, $"{tileId}.jpg").Replace("\\", "/");
|
||||
if (!File.Exists(orthoPath) && !string.IsNullOrWhiteSpace(orthoDirFallback))
|
||||
{
|
||||
string fallbackPath = Path.Combine(orthoDirFallback, $"{tileId}.jpg").Replace("\\", "/");
|
||||
if (File.Exists(fallbackPath))
|
||||
orthoPath = fallbackPath;
|
||||
}
|
||||
if (!File.Exists(orthoPath))
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Ortho texture missing for {tileId}: {orthoPath}");
|
||||
@@ -526,6 +866,9 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
private void AddBuildings(GameObject root, TileMetadata tile)
|
||||
{
|
||||
if (!ShouldIncludeBuildings(tile, includeBuildingsEvenTilesOnly))
|
||||
return;
|
||||
|
||||
string glbPath = Path.Combine(buildingsDir, $"{tile.TileId}.glb").Replace("\\", "/");
|
||||
if (!File.Exists(glbPath))
|
||||
return;
|
||||
@@ -537,17 +880,16 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
return;
|
||||
}
|
||||
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
// Building GLB vertices have absolute Z (elevation) from CityGML.
|
||||
// Since prefab root will be at Y=baseMin when placed, offset buildings by -baseMin
|
||||
// so building world Y = baseMin + (-baseMin) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
// Since prefab root will be at Y=tile_min when placed, offset buildings by -tile_min
|
||||
// so building world Y = tile_min + (-tile_min) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
var instance = PrefabUtility.InstantiatePrefab(buildingPrefab) as GameObject;
|
||||
if (instance == null)
|
||||
instance = Instantiate(buildingPrefab);
|
||||
|
||||
instance.name = "Buildings";
|
||||
instance.transform.SetParent(root.transform, false);
|
||||
instance.transform.localPosition = new Vector3(0f, -baseMin, 0f);
|
||||
instance.transform.localPosition = new Vector3(0f, -(float)tile.TileMin, 0f);
|
||||
instance.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
instance.isStatic = true;
|
||||
}
|
||||
@@ -568,13 +910,12 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
return;
|
||||
}
|
||||
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
// Tree GLB vertices use absolute elevation (z_ground from DGM).
|
||||
// Since prefab root will be at Y=baseMin when placed, offset trees by -baseMin
|
||||
// so tree world Y = baseMin + (-baseMin) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
// Since prefab root will be at Y=tile_min when placed, offset trees by -tile_min
|
||||
// so tree world Y = tile_min + (-tile_min) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
var treesContainer = new GameObject("Trees");
|
||||
treesContainer.transform.SetParent(root.transform, false);
|
||||
treesContainer.transform.localPosition = new Vector3(0f, -baseMin, 0f);
|
||||
treesContainer.transform.localPosition = new Vector3(0f, -(float)tile.TileMin, 0f);
|
||||
treesContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
treesContainer.isStatic = true;
|
||||
|
||||
@@ -634,7 +975,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
furnitureContainer.transform.localPosition = Vector3.zero;
|
||||
furnitureContainer.isStatic = true;
|
||||
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
float baseY = (float)tile.TileMin;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -654,7 +995,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
GameObject obj = CreateFurnitureObject(furnitureType, height, i);
|
||||
obj.transform.SetParent(furnitureContainer.transform, false);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
|
||||
obj.isStatic = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -749,7 +1090,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
treesContainer.transform.localPosition = Vector3.zero;
|
||||
treesContainer.isStatic = true;
|
||||
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
float baseY = (float)tile.TileMin;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -779,7 +1120,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
GameObject treeObj = CreateEnhancedTree(height, radius, canopyColor, i);
|
||||
treeObj.transform.SetParent(treesContainer.transform, false);
|
||||
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
|
||||
treeObj.isStatic = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
Reference in New Issue
Block a user