Use per-tile heightmap scaling
This commit is contained in:
@@ -47,7 +47,7 @@ Visual documentation showing how the GeoData pipeline transforms raw geospatial
|
||||
│ *.png (16-bit) │ *.jpg (2048²) │ *.glb │ *.csv │
|
||||
│ *.pgw │ *.jgw │ │ trees_tiles/*.glb │
|
||||
├─────────────────┴─────────────────┴─────────────────┴───────────────────────┤
|
||||
│ tile_index.csv (manifest with bounds + elevation range + tile_key) │
|
||||
│ tile_index.csv (bounds + global/tile min/max + tile_key) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ export_unity/ directory │
|
||||
└────────────────────────────────────────┬────────────────────────────────────┘
|
||||
@@ -118,7 +118,7 @@ Visual documentation showing how the GeoData pipeline transforms raw geospatial
|
||||
raw/dgm1/*.tif ──► work/dgm.vrt ──► Per-tile warp (1025×1025)
|
||||
│
|
||||
▼
|
||||
Scale to UInt16
|
||||
Scale to UInt16 (per-tile min/max by default)
|
||||
│
|
||||
▼
|
||||
export_unity/height_png16/{tile}.png
|
||||
@@ -208,7 +208,7 @@ TREES-ENHANCED:
|
||||
| `trees/*.csv` | CSV | - | Tree positions (x,y,z,h,r) |
|
||||
| `trees_tiles/*.glb` | glTF Binary | - | Proxy geometry chunks |
|
||||
| `street_furniture/*.csv` | CSV | - | Furniture positions |
|
||||
| `tile_index.csv` | CSV | - | Manifest (bounds, min/max, tile_key) |
|
||||
| `tile_index.csv` | CSV | - | Manifest (bounds, global/tile min/max, tile_key) |
|
||||
|
||||
---
|
||||
|
||||
@@ -255,7 +255,7 @@ Config
|
||||
├── archives: ArchiveConfig # Archive staging
|
||||
├── work: WorkConfig # Temp/intermediate
|
||||
├── export: ExportConfig # Output directories
|
||||
├── heightmap: HeightmapConfig # out_res=1025, tile_size_m=1000
|
||||
├── heightmap: HeightmapConfig # out_res=1025, tile_size_m=1000, use_tile_minmax=true
|
||||
├── ortho: OrthoConfig # out_res=2048, quality=90
|
||||
├── tile_key: TileKeyConfig # tile_size_x/y=1000, overlap_x/y=0.5
|
||||
├── buildings: BuildingConfig # triangle budget 200-350k
|
||||
|
||||
@@ -17,7 +17,7 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
- `archive/` — offline storage for untouched downloads (e.g., zipped DOP/CityGML tiles, dop20 filelist).
|
||||
- `work/` — intermediates such as `dgm.vrt` and `_tmp.tif` files; safe to delete/regenerate.
|
||||
- `export_unity/height_png16/` — final 16-bit PNG heightmaps for Unity import.
|
||||
- `export_unity/tile_index.csv` — manifest mapping tile IDs to world bounds and global min/max used for scaling, plus `tile_key`; built during heightmap export and required by orthophotos.
|
||||
- `export_unity/tile_index.csv` — manifest mapping tile IDs to world bounds and scaling ranges (`global_min/global_max` plus per-tile `tile_min/tile_max`), plus `tile_key`; built during heightmap export and required by orthophotos.
|
||||
- `export_unity/ortho_jpg/` — cropped orthophoto tiles aligned to the terrain grid (JPEG + worldfiles).
|
||||
- `geodata_to_unity.py` — main CLI (uses `geodata_pipeline/` library modules).
|
||||
- `scripts/` — helpers to create the directory tree and fetch DOP20 inputs.
|
||||
@@ -35,10 +35,10 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
# buildings: uv run python geodata_to_unity.py --export buildings
|
||||
# trees (CSV+GLB): uv run python geodata_to_unity.py --export trees
|
||||
```
|
||||
4. Import the PNGs into Unity Terrains using `tile_index.csv` for placement and consistent height scaling (0–65535).
|
||||
4. Import the PNGs into Unity Terrains using `tile_index.csv` for placement and height scaling (0–65535, per-tile by default).
|
||||
|
||||
### How the export works
|
||||
- Heightmaps: the pipeline builds `work/dgm.vrt` from all `raw/dgm1/*.tif`, computes a global min/max once, and warps each tile footprint to `heightmap.out_res` with `srcNodata=-9999` and `dstNodata` set to the global min. PNGs are scaled to `[0, 65535]` with worldfiles and listed in `export_unity/tile_index.csv` with `tile_key = f"{floor((xmin + overlap_x) / tile_size_x)}_{floor((ymin + overlap_y) / tile_size_y)}"` (defaults: `tile_size_x=1000.0`, `tile_size_y=1000.0`, `overlap_x=0.5`, `overlap_y=0.5` in `[tile_key]`).
|
||||
- Heightmaps: the pipeline builds `work/dgm.vrt` from all `raw/dgm1/*.tif`, computes a global min/max once (legacy fallback), and warps each tile footprint to `heightmap.out_res` with `srcNodata=-9999`. Per-tile min/max are computed from the warped tile and used to scale PNGs to `[0, 65535]` by default (`heightmap.use_tile_minmax=false` keeps global scaling). `export_unity/tile_index.csv` records `global_min/global_max`, `tile_min/tile_max`, and `tile_key = f"{floor((xmin + overlap_x) / tile_size_x)}_{floor((ymin + overlap_y) / tile_size_y)}"` (defaults: `tile_size_x=1000.0`, `tile_size_y=1000.0`, `overlap_x=0.5`, `overlap_y=0.5` in `[tile_key]`).
|
||||
- Orthophotos: `work/dop.vrt` is built from `raw/dop20/jp2/*.jp2`; the manifest drives the cropping bounds. JPEG tiles are written to `export_unity/ortho_jpg/` with matching `.jgw` worldfiles. If the manifest is missing, the orthophoto export aborts—run the heightmap export first or use `--export all`.
|
||||
- Archives: `--build-from-archive` expands every `*.zip` under `archive/*` into the matching `raw/*` directories and copies `archive/dop20/filelist.txt` next to `raw/dop20/` for the downloader.
|
||||
- Cleanup: temporary `_tmp.tif` and GDAL aux XML files under `work/` and `raw/dgm1/` are removed at the end of the heightmap export; avoid storing non-GDAL metadata in those folders.
|
||||
@@ -52,7 +52,7 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
- Rebuild VRTs after moving data: add `--force-vrt`.
|
||||
|
||||
### Workflow Notes
|
||||
- The pipeline computes a global min/max from the VRT to scale all tiles consistently; adjust `heightmap.out_res` or `heightmap.resample` in `geodata_config.toml` if your AOI or target resolution changes.
|
||||
- Heightmaps normalize per tile using `tile_min/tile_max` by default; set `heightmap.use_tile_minmax=false` to restore global scaling across tiles. Adjust `heightmap.out_res` or `heightmap.resample` in `geodata_config.toml` if your AOI or target resolution changes.
|
||||
- `tile_key` config controls the tile grouping key in the manifest; defaults are `tile_size_x=1000.0`, `tile_size_y=1000.0`, `overlap_x=0.5`, `overlap_y=0.5` with `enabled=true`.
|
||||
- `_tmp.tif` files in `work/` are transient; you can delete `work/` to force a clean rebuild.
|
||||
- Keep file names stable to avoid churn in Unity scenes; re-exports overwrite in place.
|
||||
|
||||
@@ -25,6 +25,7 @@ manifest_path = "export_unity/tile_index.csv"
|
||||
out_res = 1025
|
||||
resample = "bilinear"
|
||||
tile_size_m = 1000
|
||||
use_tile_minmax = true
|
||||
|
||||
[tile_key]
|
||||
tile_size_x = 1000.0
|
||||
|
||||
@@ -46,6 +46,7 @@ class HeightmapConfig:
|
||||
out_res: int = 1025
|
||||
resample: str = "bilinear"
|
||||
tile_size_m: int = 1000
|
||||
use_tile_minmax: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -5,6 +5,7 @@ import math
|
||||
import os
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
from osgeo import gdal
|
||||
|
||||
from .config import Config
|
||||
@@ -22,6 +23,29 @@ def _cleanup_patterns(raw_dir: str) -> Iterable[str]:
|
||||
]
|
||||
|
||||
|
||||
def _compute_tile_minmax(path: str) -> tuple[float | None, float | None, int]:
|
||||
ds = open_dataset(path, f"Could not open {path} to compute tile min/max.")
|
||||
band = ds.GetRasterBand(1)
|
||||
nodata = band.GetNoDataValue()
|
||||
data = band.ReadAsArray()
|
||||
ds = None
|
||||
|
||||
if data is None or data.size == 0:
|
||||
return None, None, 0
|
||||
|
||||
valid = np.isfinite(data)
|
||||
if nodata is not None and not math.isnan(nodata):
|
||||
valid &= data != nodata
|
||||
|
||||
valid_count = int(np.count_nonzero(valid))
|
||||
if valid_count == 0:
|
||||
return None, None, 0
|
||||
|
||||
tile_min = float(data[valid].min())
|
||||
tile_max = float(data[valid].max())
|
||||
return tile_min, tile_max, valid_count
|
||||
|
||||
|
||||
def export_heightmaps(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
ensure_dir(cfg.work.work_dir)
|
||||
ensure_dir(cfg.export.heightmap_dir)
|
||||
@@ -40,7 +64,7 @@ def export_heightmaps(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
tile_key_seen: set[str] = set()
|
||||
|
||||
with open(cfg.export.manifest_path, "w", encoding="utf-8") as f:
|
||||
f.write("tile_id,xmin,ymin,xmax,ymax,global_min,global_max,out_res,tile_key\n")
|
||||
f.write("tile_id,xmin,ymin,xmax,ymax,global_min,global_max,out_res,tile_key,tile_min,tile_max\n")
|
||||
|
||||
skipped = 0
|
||||
written = 0
|
||||
@@ -91,9 +115,31 @@ def export_heightmaps(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
tile_min, tile_max, valid_count = _compute_tile_minmax(tmp_path)
|
||||
except SystemExit as exc:
|
||||
print(exc)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
tile_valid = valid_count > 0
|
||||
if not tile_valid:
|
||||
print(f"Warning: {tile_id} has no valid samples; using global min/max.")
|
||||
tile_min = gmin
|
||||
tile_max = gmax
|
||||
|
||||
scale_min = gmin
|
||||
scale_max = gmax
|
||||
if cfg.heightmap.use_tile_minmax and tile_valid:
|
||||
scale_min = tile_min
|
||||
scale_max = tile_max
|
||||
if scale_max <= scale_min:
|
||||
print(f"Warning: {tile_id} has flat elevation range; using epsilon for scaling.")
|
||||
scale_max = scale_min + 1e-3
|
||||
|
||||
trans_opts = gdal.TranslateOptions(
|
||||
outputType=gdal.GDT_UInt16,
|
||||
scaleParams=[(gmin, gmax, 0, 65535)],
|
||||
scaleParams=[(scale_min, scale_max, 0, 65535)],
|
||||
format="PNG",
|
||||
creationOptions=["WORLDFILE=YES"],
|
||||
)
|
||||
@@ -107,7 +153,8 @@ def export_heightmaps(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
safe_remove(f"{tmp_path}.aux.xml")
|
||||
|
||||
f.write(
|
||||
f"{tile_id},{xmin},{ymin},{xmax},{ymax},{gmin},{gmax},{cfg.heightmap.out_res},{tile_key}\n"
|
||||
f"{tile_id},{xmin},{ymin},{xmax},{ymax},{gmin},{gmax},"
|
||||
f"{cfg.heightmap.out_res},{tile_key},{tile_min},{tile_max}\n"
|
||||
)
|
||||
print(f"Wrote {out_path}")
|
||||
written += 1
|
||||
|
||||
@@ -116,7 +116,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 (or 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);
|
||||
}
|
||||
@@ -298,6 +298,9 @@ public class GeoTileImporter : EditorWindow
|
||||
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;
|
||||
@@ -312,6 +315,8 @@ public class GeoTileImporter : EditorWindow
|
||||
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}'");
|
||||
@@ -342,7 +347,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 baseMin)>();
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -351,6 +356,8 @@ public class GeoTileImporter : EditorWindow
|
||||
|
||||
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++;
|
||||
@@ -361,6 +368,9 @@ public class GeoTileImporter : EditorWindow
|
||||
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
|
||||
{
|
||||
@@ -377,15 +387,40 @@ public class GeoTileImporter : EditorWindow
|
||||
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}.");
|
||||
|
||||
float heightRange = (float)(gmax - gmin);
|
||||
double baseMin = tileRangeValid ? tileMin : gmin;
|
||||
double baseMax = tileRangeValid ? tileMax : gmax;
|
||||
float heightRange = (float)(baseMax - baseMin);
|
||||
if (heightRange <= 0.0001f)
|
||||
{
|
||||
skipped++;
|
||||
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (global_max <= global_min). Skipping.");
|
||||
continue;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
string pngPath = Path.Combine(heightmapsDir, $"{tileId}.png").Replace("\\", "/");
|
||||
@@ -452,7 +487,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;
|
||||
@@ -493,9 +528,9 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[GeoTileImporter] Imported {tileId} @ XZ=({ux},{uz}) Y={gmin} heightRange={heightRange} usedU16={usedU16}");
|
||||
Debug.Log($"[GeoTileImporter] Imported {tileId} @ 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}'.");
|
||||
@@ -509,7 +544,7 @@ public class GeoTileImporter : EditorWindow
|
||||
ImportEnhancedTrees(placements);
|
||||
}
|
||||
|
||||
private void ImportBuildings(List<(string tileId, float ux, float uz, float gmin)> placements)
|
||||
private void ImportBuildings(List<(string tileId, float ux, float uz, float baseMin)> placements)
|
||||
{
|
||||
if (!importBuildings)
|
||||
return;
|
||||
@@ -533,7 +568,7 @@ public class GeoTileImporter : EditorWindow
|
||||
}
|
||||
|
||||
int imported = 0, missing = 0;
|
||||
foreach (var (tileId, ux, uz, gmin) in placements)
|
||||
foreach (var (tileId, ux, uz, baseMin) in placements)
|
||||
{
|
||||
string glbPath = Path.Combine(activeDir, $"{tileId}.glb").Replace("\\", "/");
|
||||
if (!File.Exists(glbPath))
|
||||
@@ -554,7 +589,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, baseMin, uz);
|
||||
inst.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
inst.isStatic = true;
|
||||
imported++;
|
||||
@@ -563,7 +598,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 baseMin)> placements)
|
||||
{
|
||||
if (!importTrees)
|
||||
return;
|
||||
@@ -588,7 +623,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, baseMin) in placements)
|
||||
{
|
||||
// Look for chunk files: {tileId}_0_0.glb, {tileId}_0_1.glb, etc.
|
||||
// Standard tree export creates 4x4 chunks per tile
|
||||
@@ -612,7 +647,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, baseMin, uz);
|
||||
tileContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
@@ -640,7 +675,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 baseMin)> placements)
|
||||
{
|
||||
if (!importFurniture)
|
||||
return;
|
||||
@@ -662,7 +697,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, baseMin) in placements)
|
||||
{
|
||||
string csvPath = Path.Combine(furnitureDir, $"{tileId}.csv").Replace("\\", "/");
|
||||
if (!File.Exists(csvPath))
|
||||
@@ -696,7 +731,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, baseMin, uz);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
@@ -767,7 +802,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 - baseMin, yLocal);
|
||||
obj.isStatic = true;
|
||||
imported++;
|
||||
}
|
||||
@@ -782,7 +817,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 baseMin)> placements)
|
||||
{
|
||||
if (!importEnhancedTrees)
|
||||
return;
|
||||
@@ -804,7 +839,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, baseMin) in placements)
|
||||
{
|
||||
string csvPath = Path.Combine(enhancedTreesDir, $"{tileId}.csv").Replace("\\", "/");
|
||||
if (!File.Exists(csvPath))
|
||||
@@ -842,7 +877,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, baseMin, uz);
|
||||
tileContainer.isStatic = true;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
@@ -940,7 +975,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 - baseMin, yLocal);
|
||||
treeObj.isStatic = true;
|
||||
imported++;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
public string TileId;
|
||||
public double Xmin, Ymin, Xmax, Ymax;
|
||||
public double GlobalMin, GlobalMax;
|
||||
public double TileMin, TileMax;
|
||||
public bool HasTileMinMax;
|
||||
public int OutRes;
|
||||
}
|
||||
|
||||
@@ -288,6 +290,9 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
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;
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -296,6 +301,8 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
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));
|
||||
if (parts.Length <= maxIdx)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Skipping line {i + 1}: too few columns.");
|
||||
@@ -304,6 +311,26 @@ 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.");
|
||||
}
|
||||
}
|
||||
|
||||
tiles.Add(new TileMetadata
|
||||
{
|
||||
TileId = parts[IDX_TILE].Trim(),
|
||||
@@ -311,8 +338,11 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
Ymin = double.Parse(parts[IDX_YMIN], ci),
|
||||
Xmax = double.Parse(parts[IDX_XMAX], ci),
|
||||
Ymax = double.Parse(parts[IDX_YMAX], ci),
|
||||
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
|
||||
GlobalMax = double.Parse(parts[IDX_GMAX], ci),
|
||||
GlobalMin = gmin,
|
||||
GlobalMax = gmax,
|
||||
TileMin = tileMin,
|
||||
TileMax = tileMax,
|
||||
HasTileMinMax = tileRangeValid,
|
||||
OutRes = int.Parse(parts[IDX_RES], ci)
|
||||
});
|
||||
}
|
||||
@@ -328,11 +358,21 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
private bool CreateTilePrefab(TileMetadata tile)
|
||||
{
|
||||
// Validate height range
|
||||
float heightRange = (float)(tile.GlobalMax - tile.GlobalMin);
|
||||
double baseMin = tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin;
|
||||
double baseMax = tile.HasTileMinMax ? tile.TileMax : tile.GlobalMax;
|
||||
float heightRange = (float)(baseMax - baseMin);
|
||||
if (heightRange <= 0.0001f)
|
||||
{
|
||||
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {tile.TileId}: invalid height range. Skipping.");
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Load heightmap
|
||||
@@ -412,6 +452,9 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
metadata.ymin = tile.Ymin;
|
||||
metadata.globalMin = tile.GlobalMin;
|
||||
metadata.globalMax = tile.GlobalMax;
|
||||
metadata.tileMin = tile.TileMin;
|
||||
metadata.tileMax = tile.TileMax;
|
||||
metadata.hasTileMinMax = tile.HasTileMinMax;
|
||||
|
||||
// Add child components
|
||||
if (includeBuildings)
|
||||
@@ -494,16 +537,17 @@ 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=gmin when placed, offset buildings by -gmin
|
||||
// so building world Y = gmin + (-gmin) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
// 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)
|
||||
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, -(float)tile.GlobalMin, 0f);
|
||||
instance.transform.localPosition = new Vector3(0f, -baseMin, 0f);
|
||||
instance.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
instance.isStatic = true;
|
||||
}
|
||||
@@ -524,12 +568,13 @@ 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=gmin when placed, offset trees by -gmin
|
||||
// so tree world Y = gmin + (-gmin) + GLB_Y = GLB_Y (correct absolute elevation)
|
||||
// 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)
|
||||
var treesContainer = new GameObject("Trees");
|
||||
treesContainer.transform.SetParent(root.transform, false);
|
||||
treesContainer.transform.localPosition = new Vector3(0f, -(float)tile.GlobalMin, 0f);
|
||||
treesContainer.transform.localPosition = new Vector3(0f, -baseMin, 0f);
|
||||
treesContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
||||
treesContainer.isStatic = true;
|
||||
|
||||
@@ -589,7 +634,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
furnitureContainer.transform.localPosition = Vector3.zero;
|
||||
furnitureContainer.isStatic = true;
|
||||
|
||||
float gmin = (float)tile.GlobalMin;
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -609,7 +654,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
|
||||
GameObject obj = CreateFurnitureObject(furnitureType, height, i);
|
||||
obj.transform.SetParent(furnitureContainer.transform, false);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - gmin, yLocal);
|
||||
obj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
obj.isStatic = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -704,7 +749,7 @@ public class GeoTilePrefabImporter : EditorWindow
|
||||
treesContainer.transform.localPosition = Vector3.zero;
|
||||
treesContainer.isStatic = true;
|
||||
|
||||
float gmin = (float)tile.GlobalMin;
|
||||
float baseMin = (float)(tile.HasTileMinMax ? tile.TileMin : tile.GlobalMin);
|
||||
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -734,7 +779,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 - gmin, yLocal);
|
||||
treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
|
||||
treeObj.isStatic = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -11,15 +11,19 @@ public class GeoTileMetadata : MonoBehaviour
|
||||
public double ymin;
|
||||
public double globalMin;
|
||||
public double globalMax;
|
||||
public double tileMin;
|
||||
public double tileMax;
|
||||
public bool hasTileMinMax;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the world position this tile should be placed at, given a global origin.
|
||||
/// </summary>
|
||||
public Vector3 GetWorldPosition(double originX, double originY)
|
||||
{
|
||||
double baseMin = hasTileMinMax ? tileMin : globalMin;
|
||||
return new Vector3(
|
||||
(float)(xmin - originX),
|
||||
(float)globalMin,
|
||||
(float)baseMin,
|
||||
(float)(ymin - originY)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user