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:
2026-02-03 23:02:37 +01:00
parent 196c8b9890
commit 1a0ab1e4b6
11 changed files with 1967 additions and 280 deletions

View File

@@ -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)