add per-tile min/max elevation support

This commit is contained in:
2026-01-23 16:42:17 +01:00
parent 04ef2c68cc
commit bdbd0eb99b
5 changed files with 111 additions and 42 deletions

View File

@@ -149,7 +149,7 @@ public class GeoTileImporter : EditorWindow
EditorGUILayout.HelpBox(
"Creates one Unity Terrain per CSV row and positions tiles on a meter grid.\n" +
"Absolute elevation mapping: Terrain Y = global_min, Terrain height = (global_max - global_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);
@@ -336,7 +336,7 @@ public class GeoTileImporter : EditorWindow
int imported = 0, skipped = 0;
int importedTextures = 0;
var placements = new List<(string tileId, float ux, float uz, float gmin)>();
var placements = new List<(string tileId, float ux, float uz, float baseY)>();
for (int i = 0; i < selectedTiles.Count; i++)
{
@@ -344,17 +344,17 @@ public class GeoTileImporter : EditorWindow
var tileId = tile.TileId;
double xmin = tile.Xmin;
double ymin = tile.Ymin;
double gmin = tile.GlobalMin;
double gmax = tile.GlobalMax;
double baseMin = tile.TileMin;
double baseMax = tile.TileMax;
if (tile.OutRes != heightmapResolution)
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={tile.OutRes} but importer expects {heightmapResolution}.");
float heightRange = (float)(gmax - gmin);
float heightRange = (float)(baseMax - baseMin);
if (heightRange <= 0.0001f)
{
skipped++;
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (global_max <= global_min). Skipping.");
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (tile_max <= tile_min). Skipping.");
continue;
}
@@ -422,7 +422,7 @@ public class GeoTileImporter : EditorWindow
float ux = (float)(xmin - originX);
float uz = (float)(ymin - originY);
go.transform.position = new Vector3(ux, (float)gmin, uz);
go.transform.position = new Vector3(ux, (float)baseMin, uz);
var terrain = go.GetComponent<Terrain>();
terrain.drawInstanced = true;
@@ -463,9 +463,9 @@ public class GeoTileImporter : EditorWindow
}
}
Debug.Log($"[GeoTileImporter] Imported {tileId} ({tile.TileKey}) @ XZ=({ux},{uz}) Y={gmin} 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)gmin));
placements.Add((tileId, ux, uz, (float)baseMin));
}
Debug.Log($"[GeoTileImporter] DONE. Imported={imported}, Skipped={skipped}, OrthoApplied={importedTextures} under '{parentName}'.");
@@ -672,6 +672,8 @@ public class GeoTileImporter : EditorWindow
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++)
@@ -684,6 +686,10 @@ public class GeoTileImporter : EditorWindow
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)
{
@@ -697,6 +703,16 @@ public class GeoTileImporter : EditorWindow
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
{
@@ -706,8 +722,10 @@ public class GeoTileImporter : EditorWindow
YKey = yKey,
Xmin = xmin,
Ymin = ymin,
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
GlobalMax = double.Parse(parts[IDX_GMAX], ci),
GlobalMin = globalMin,
GlobalMax = globalMax,
TileMin = tileMin,
TileMax = tileMax,
OutRes = int.Parse(parts[IDX_RES], ci)
});
}
@@ -775,6 +793,8 @@ public class GeoTileImporter : EditorWindow
public double Ymin;
public double GlobalMin;
public double GlobalMax;
public double TileMin;
public double TileMax;
public int OutRes;
}
@@ -786,7 +806,7 @@ public class GeoTileImporter : EditorWindow
}
private void ImportBuildings(List<(string tileId, float ux, float uz, float gmin)> placements)
private void ImportBuildings(List<(string tileId, float ux, float uz, float baseY)> placements)
{
if (!importBuildings)
return;
@@ -810,7 +830,7 @@ public class GeoTileImporter : EditorWindow
}
int imported = 0, missing = 0;
foreach (var (tileId, ux, uz, gmin) in placements)
foreach (var (tileId, ux, uz, baseY) in placements)
{
string glbPath = Path.Combine(activeDir, $"{tileId}.glb").Replace("\\", "/");
if (!File.Exists(glbPath))
@@ -831,7 +851,7 @@ public class GeoTileImporter : EditorWindow
var inst = PrefabUtility.InstantiatePrefab(prefab) as GameObject ?? Instantiate(prefab);
inst.name = tileId;
inst.transform.SetParent(parent.transform, false);
inst.transform.position = new Vector3(ux, gmin, uz);
inst.transform.position = new Vector3(ux, baseY, uz);
inst.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
inst.isStatic = true;
imported++;
@@ -840,7 +860,7 @@ public class GeoTileImporter : EditorWindow
Debug.Log($"[GeoTileImporter] Buildings ({sourceLabel}) imported={imported}, missing/failed={missing} under '{buildingsParentName}'.");
}
private void ImportTrees(List<(string tileId, float ux, float uz, float gmin)> placements)
private void ImportTrees(List<(string tileId, float ux, float uz, float baseY)> placements)
{
if (!importTrees)
return;
@@ -865,7 +885,7 @@ public class GeoTileImporter : EditorWindow
}
int importedTiles = 0, importedChunks = 0, missingTiles = 0;
foreach (var (tileId, ux, uz, gmin) 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
@@ -889,7 +909,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, gmin, uz);
tileContainer.transform.position = new Vector3(ux, baseY, uz);
tileContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
tileContainer.isStatic = true;
@@ -917,7 +937,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 gmin)> placements)
private void ImportFurniture(List<(string tileId, float ux, float uz, float baseY)> placements)
{
if (!importFurniture)
return;
@@ -939,7 +959,7 @@ public class GeoTileImporter : EditorWindow
int imported = 0, skipped = 0;
var ci = CultureInfo.InvariantCulture;
foreach (var (tileId, ux, uz, gmin) in placements)
foreach (var (tileId, ux, uz, baseY) in placements)
{
string csvPath = Path.Combine(furnitureDir, $"{tileId}.csv").Replace("\\", "/");
if (!File.Exists(csvPath))
@@ -973,7 +993,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, gmin, uz);
tileContainer.transform.position = new Vector3(ux, baseY, uz);
tileContainer.isStatic = true;
for (int i = 1; i < lines.Length; i++)
@@ -1044,7 +1064,7 @@ public class GeoTileImporter : EditorWindow
}
obj.transform.SetParent(tileContainer.transform, false);
obj.transform.localPosition = new Vector3(xLocal, zGround - gmin, yLocal);
obj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
obj.isStatic = true;
imported++;
}
@@ -1059,7 +1079,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 gmin)> placements)
private void ImportEnhancedTrees(List<(string tileId, float ux, float uz, float baseY)> placements)
{
if (!importEnhancedTrees)
return;
@@ -1081,7 +1101,7 @@ public class GeoTileImporter : EditorWindow
int imported = 0, skipped = 0;
var ci = CultureInfo.InvariantCulture;
foreach (var (tileId, ux, uz, gmin) in placements)
foreach (var (tileId, ux, uz, baseY) in placements)
{
string csvPath = Path.Combine(enhancedTreesDir, $"{tileId}.csv").Replace("\\", "/");
if (!File.Exists(csvPath))
@@ -1119,7 +1139,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, gmin, uz);
tileContainer.transform.position = new Vector3(ux, baseY, uz);
tileContainer.isStatic = true;
for (int i = 1; i < lines.Length; i++)
@@ -1217,7 +1237,7 @@ public class GeoTileImporter : EditorWindow
}
treeObj.transform.SetParent(tileContainer.transform, false);
treeObj.transform.localPosition = new Vector3(xLocal, zGround - gmin, yLocal);
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseY, yLocal);
treeObj.isStatic = true;
imported++;
}