963 lines
36 KiB
C#
963 lines
36 KiB
C#
// Assets/Editor/GeoTilePrefabImporter.cs
|
|
// Creates prefab assets from geo tiles. Each prefab contains terrain + buildings + trees + furniture.
|
|
// Unlike GeoTileImporter (which creates scene objects), this importer produces reusable .prefab assets.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public class GeoTilePrefabImporter : EditorWindow
|
|
{
|
|
// Input paths (same as GeoTileImporter)
|
|
private string tilesCsvPath = "Assets/GeoData/tile_index.csv";
|
|
private string heightmapsDir = "Assets/GeoData/height_png16";
|
|
private string orthoDir = "Assets/GeoData/ortho_jpg";
|
|
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 bool overwriteExisting = false;
|
|
|
|
// Terrain settings
|
|
private float tileSizeMeters = 1000f;
|
|
private int heightmapResolution = 1025;
|
|
|
|
// Component toggles
|
|
private bool applyOrthoTextures = true;
|
|
private bool includeBuildings = true;
|
|
private bool includeTrees = true;
|
|
private bool includeFurniture = false;
|
|
private bool includeEnhancedTrees = false;
|
|
|
|
// Prefabs for furniture (optional)
|
|
private GameObject lampPrefab;
|
|
private GameObject benchPrefab;
|
|
private GameObject signPrefab;
|
|
private GameObject bollardPrefab;
|
|
private GameObject defaultFurniturePrefab;
|
|
private GameObject treePrefab;
|
|
|
|
private struct TileMetadata
|
|
{
|
|
public string TileId;
|
|
public double Xmin, Ymin, Xmax, Ymax;
|
|
public double GlobalMin, GlobalMax;
|
|
public int OutRes;
|
|
}
|
|
|
|
[MenuItem("Tools/Geo Tiles/Import Tiles as Prefabs")]
|
|
public static void ShowWindow()
|
|
{
|
|
var win = GetWindow<GeoTilePrefabImporter>("Geo Tile Prefab Importer");
|
|
win.minSize = new Vector2(620, 480);
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
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);
|
|
buildingsDir = EditorGUILayout.TextField("buildings_tiles dir", buildingsDir);
|
|
treesDir = EditorGUILayout.TextField("trees_tiles dir", treesDir);
|
|
furnitureDir = EditorGUILayout.TextField("street_furniture dir", furnitureDir);
|
|
enhancedTreesDir = EditorGUILayout.TextField("trees_enhanced dir", enhancedTreesDir);
|
|
|
|
GUILayout.Space(10);
|
|
GUILayout.Label("Output Settings", EditorStyles.boldLabel);
|
|
prefabOutputDir = EditorGUILayout.TextField("Prefab output dir", prefabOutputDir);
|
|
overwriteExisting = EditorGUILayout.ToggleLeft("Overwrite existing prefabs", overwriteExisting);
|
|
|
|
GUILayout.Space(10);
|
|
GUILayout.Label("Terrain Settings", EditorStyles.boldLabel);
|
|
tileSizeMeters = EditorGUILayout.FloatField("Tile size (m)", tileSizeMeters);
|
|
heightmapResolution = EditorGUILayout.IntField("Heightmap resolution", heightmapResolution);
|
|
|
|
GUILayout.Space(10);
|
|
GUILayout.Label("Include Components", EditorStyles.boldLabel);
|
|
applyOrthoTextures = EditorGUILayout.ToggleLeft("Apply ortho textures", applyOrthoTextures);
|
|
includeBuildings = EditorGUILayout.ToggleLeft("Include buildings (GLB)", includeBuildings);
|
|
includeTrees = EditorGUILayout.ToggleLeft("Include trees (GLB chunks)", includeTrees);
|
|
includeFurniture = EditorGUILayout.ToggleLeft("Include street furniture (CSV)", includeFurniture);
|
|
includeEnhancedTrees = EditorGUILayout.ToggleLeft("Include enhanced trees (CSV)", includeEnhancedTrees);
|
|
|
|
GUILayout.Space(10);
|
|
GUILayout.Label("Prefabs (optional, for furniture/trees)", EditorStyles.boldLabel);
|
|
treePrefab = (GameObject)EditorGUILayout.ObjectField("Tree Prefab", treePrefab, typeof(GameObject), false);
|
|
lampPrefab = (GameObject)EditorGUILayout.ObjectField("Lamp Prefab", lampPrefab, typeof(GameObject), false);
|
|
benchPrefab = (GameObject)EditorGUILayout.ObjectField("Bench Prefab", benchPrefab, typeof(GameObject), false);
|
|
signPrefab = (GameObject)EditorGUILayout.ObjectField("Sign Prefab", signPrefab, typeof(GameObject), false);
|
|
bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
|
|
defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture", defaultFurniturePrefab, typeof(GameObject), false);
|
|
|
|
GUILayout.Space(15);
|
|
if (GUILayout.Button("Generate Prefabs"))
|
|
ImportTilesAsPrefabs();
|
|
|
|
GUILayout.Space(10);
|
|
if (GUILayout.Button("Place All Prefabs in Scene"))
|
|
PlaceAllPrefabsInScene();
|
|
|
|
EditorGUILayout.HelpBox(
|
|
"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" +
|
|
"IMPORTANT: Use 'Place All Prefabs in Scene' to position tiles correctly.",
|
|
MessageType.Info);
|
|
}
|
|
|
|
private void PlaceAllPrefabsInScene()
|
|
{
|
|
if (!Directory.Exists(prefabOutputDir))
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] Prefab directory not found: {prefabOutputDir}");
|
|
return;
|
|
}
|
|
|
|
var prefabFiles = Directory.GetFiles(prefabOutputDir, "*.prefab");
|
|
if (prefabFiles.Length == 0)
|
|
{
|
|
Debug.LogError("[GeoTilePrefabImporter] No prefabs found. Generate prefabs first.");
|
|
return;
|
|
}
|
|
|
|
// Load all prefabs and collect metadata to compute origin
|
|
var prefabsWithMeta = new List<(GameObject prefab, GeoTileMetadata meta)>();
|
|
double originX = double.PositiveInfinity;
|
|
double originY = double.PositiveInfinity;
|
|
|
|
foreach (var path in prefabFiles)
|
|
{
|
|
string assetPath = path.Replace("\\", "/");
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
|
if (prefab == null) continue;
|
|
|
|
var meta = prefab.GetComponent<GeoTileMetadata>();
|
|
if (meta == null)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Prefab missing GeoTileMetadata: {assetPath}");
|
|
continue;
|
|
}
|
|
|
|
prefabsWithMeta.Add((prefab, meta));
|
|
originX = Math.Min(originX, meta.xmin);
|
|
originY = Math.Min(originY, meta.ymin);
|
|
}
|
|
|
|
if (prefabsWithMeta.Count == 0)
|
|
{
|
|
Debug.LogError("[GeoTilePrefabImporter] No valid prefabs with metadata found.");
|
|
return;
|
|
}
|
|
|
|
// Create parent container
|
|
var parent = GameObject.Find("Geo_Tile_Prefabs");
|
|
if (parent == null) parent = new GameObject("Geo_Tile_Prefabs");
|
|
|
|
// Instantiate each prefab at correct position
|
|
int placed = 0;
|
|
foreach (var (prefab, meta) in prefabsWithMeta)
|
|
{
|
|
// Check if already placed
|
|
var existing = parent.transform.Find(meta.tileId);
|
|
if (existing != null)
|
|
{
|
|
Debug.Log($"[GeoTilePrefabImporter] Tile already in scene: {meta.tileId}");
|
|
continue;
|
|
}
|
|
|
|
var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
|
|
if (instance == null)
|
|
{
|
|
instance = Instantiate(prefab);
|
|
instance.name = meta.tileId;
|
|
}
|
|
|
|
instance.transform.SetParent(parent.transform, false);
|
|
instance.transform.position = meta.GetWorldPosition(originX, originY);
|
|
placed++;
|
|
|
|
Debug.Log($"[GeoTilePrefabImporter] Placed {meta.tileId} at {instance.transform.position}");
|
|
}
|
|
|
|
Debug.Log($"[GeoTilePrefabImporter] Placed {placed} tile prefabs under 'Geo_Tile_Prefabs'. Origin: ({originX}, {originY})");
|
|
}
|
|
|
|
private void ImportTilesAsPrefabs()
|
|
{
|
|
Debug.Log($"[GeoTilePrefabImporter] START\n csv={tilesCsvPath}\n output={prefabOutputDir}");
|
|
|
|
// Validate inputs
|
|
if (!File.Exists(tilesCsvPath))
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] CSV not found: {tilesCsvPath}");
|
|
return;
|
|
}
|
|
if (!Directory.Exists(heightmapsDir))
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] Heightmap dir not found: {heightmapsDir}");
|
|
return;
|
|
}
|
|
|
|
// Create output directories
|
|
EnsureDirectoryExists(prefabOutputDir);
|
|
EnsureDirectoryExists($"{prefabOutputDir}/TerrainData");
|
|
EnsureDirectoryExists($"{prefabOutputDir}/TerrainLayers");
|
|
|
|
// Parse CSV
|
|
var tiles = ParseTilesCsv();
|
|
if (tiles == null || tiles.Count == 0)
|
|
{
|
|
Debug.LogError("[GeoTilePrefabImporter] No valid tiles found in CSV.");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[GeoTilePrefabImporter] Found {tiles.Count} tiles to process.");
|
|
|
|
int created = 0, skipped = 0, failed = 0;
|
|
|
|
for (int i = 0; i < tiles.Count; i++)
|
|
{
|
|
var tile = tiles[i];
|
|
EditorUtility.DisplayProgressBar(
|
|
"Creating Tile Prefabs",
|
|
$"Processing {tile.TileId} ({i + 1}/{tiles.Count})",
|
|
(float)i / tiles.Count);
|
|
|
|
string prefabPath = $"{prefabOutputDir}/{tile.TileId}.prefab";
|
|
if (File.Exists(prefabPath) && !overwriteExisting)
|
|
{
|
|
Debug.Log($"[GeoTilePrefabImporter] Skipping existing: {tile.TileId}");
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (CreateTilePrefab(tile))
|
|
created++;
|
|
else
|
|
failed++;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] Failed to create prefab for {tile.TileId}: {e.Message}\n{e.StackTrace}");
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
EditorUtility.ClearProgressBar();
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
|
|
Debug.Log($"[GeoTilePrefabImporter] DONE. Created={created}, Skipped={skipped}, Failed={failed}");
|
|
}
|
|
|
|
private List<TileMetadata> ParseTilesCsv()
|
|
{
|
|
var tiles = new List<TileMetadata>();
|
|
var ci = CultureInfo.InvariantCulture;
|
|
var lines = File.ReadAllLines(tilesCsvPath);
|
|
|
|
if (lines.Length < 2)
|
|
{
|
|
Debug.LogError("[GeoTilePrefabImporter] CSV has no data rows.");
|
|
return null;
|
|
}
|
|
|
|
var headerMap = BuildHeaderMap(lines[0]);
|
|
string[] required = { "tile_id", "xmin", "ymin", "xmax", "ymax", "global_min", "global_max", "out_res" };
|
|
if (!HasAll(headerMap, required))
|
|
{
|
|
Debug.LogError("[GeoTilePrefabImporter] CSV missing required columns. Required: " +
|
|
string.Join(", ", required));
|
|
return null;
|
|
}
|
|
|
|
int IDX_TILE = headerMap["tile_id"];
|
|
int IDX_XMIN = headerMap["xmin"];
|
|
int IDX_YMIN = headerMap["ymin"];
|
|
int IDX_XMAX = headerMap["xmax"];
|
|
int IDX_YMAX = headerMap["ymax"];
|
|
int IDX_GMIN = headerMap["global_min"];
|
|
int IDX_GMAX = headerMap["global_max"];
|
|
int IDX_RES = headerMap["out_res"];
|
|
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
var line = lines[i].Trim();
|
|
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 (parts.Length <= maxIdx)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Skipping line {i + 1}: too few columns.");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
tiles.Add(new TileMetadata
|
|
{
|
|
TileId = parts[IDX_TILE].Trim(),
|
|
Xmin = double.Parse(parts[IDX_XMIN], ci),
|
|
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),
|
|
OutRes = int.Parse(parts[IDX_RES], ci)
|
|
});
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Parse error line {i + 1}: {e.Message}");
|
|
}
|
|
}
|
|
|
|
return tiles;
|
|
}
|
|
|
|
private bool CreateTilePrefab(TileMetadata tile)
|
|
{
|
|
// Validate height range
|
|
float heightRange = (float)(tile.GlobalMax - tile.GlobalMin);
|
|
if (heightRange <= 0.0001f)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Tile {tile.TileId}: invalid height range. Skipping.");
|
|
return false;
|
|
}
|
|
|
|
// Load heightmap
|
|
string pngPath = Path.Combine(heightmapsDir, $"{tile.TileId}.png").Replace("\\", "/");
|
|
if (!File.Exists(pngPath))
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] Missing heightmap for {tile.TileId}: {pngPath}");
|
|
return false;
|
|
}
|
|
|
|
EnsureHeightmapImportSettings(pngPath);
|
|
var heightmapTex = AssetDatabase.LoadAssetAtPath<Texture2D>(pngPath);
|
|
if (heightmapTex == null)
|
|
{
|
|
Debug.LogError($"[GeoTilePrefabImporter] Could not load heightmap: {pngPath}");
|
|
return false;
|
|
}
|
|
|
|
// Create TerrainData
|
|
var terrainData = new TerrainData
|
|
{
|
|
heightmapResolution = heightmapResolution,
|
|
size = new Vector3(tileSizeMeters, heightRange, tileSizeMeters)
|
|
};
|
|
|
|
// Read heightmap pixels
|
|
int w = heightmapTex.width;
|
|
int h = heightmapTex.height;
|
|
var heights = new float[h, w];
|
|
|
|
bool usedU16 = false;
|
|
try
|
|
{
|
|
var raw = heightmapTex.GetPixelData<ushort>(0);
|
|
if (raw.Length == w * h)
|
|
{
|
|
for (int y = 0; y < h; y++)
|
|
for (int x = 0; x < w; x++)
|
|
heights[y, x] = raw[y * w + x] / 65535f;
|
|
usedU16 = true;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
if (!usedU16)
|
|
{
|
|
var pixels = heightmapTex.GetPixels();
|
|
for (int y = 0; y < h; y++)
|
|
for (int x = 0; x < w; x++)
|
|
heights[y, x] = pixels[y * w + x].r;
|
|
}
|
|
|
|
terrainData.SetHeights(0, 0, heights);
|
|
|
|
// Save TerrainData as asset (MUST be done before creating prefab)
|
|
string terrainDataPath = $"{prefabOutputDir}/TerrainData/{tile.TileId}_TerrainData.asset";
|
|
if (File.Exists(terrainDataPath))
|
|
AssetDatabase.DeleteAsset(terrainDataPath);
|
|
AssetDatabase.CreateAsset(terrainData, terrainDataPath);
|
|
|
|
// Apply ortho texture if enabled
|
|
if (applyOrthoTextures)
|
|
{
|
|
ApplyOrthoTexture(terrainData, tile.TileId);
|
|
}
|
|
|
|
// Create root GameObject with Terrain
|
|
var root = new GameObject(tile.TileId);
|
|
var terrain = root.AddComponent<Terrain>();
|
|
terrain.terrainData = terrainData;
|
|
terrain.drawInstanced = true;
|
|
|
|
var collider = root.AddComponent<TerrainCollider>();
|
|
collider.terrainData = terrainData;
|
|
|
|
// Store metadata as component for later use
|
|
var metadata = root.AddComponent<GeoTileMetadata>();
|
|
metadata.tileId = tile.TileId;
|
|
metadata.xmin = tile.Xmin;
|
|
metadata.ymin = tile.Ymin;
|
|
metadata.globalMin = tile.GlobalMin;
|
|
metadata.globalMax = tile.GlobalMax;
|
|
|
|
// Add child components
|
|
if (includeBuildings)
|
|
AddBuildings(root, tile);
|
|
|
|
if (includeTrees)
|
|
AddTrees(root, tile);
|
|
|
|
if (includeFurniture)
|
|
AddFurniture(root, tile);
|
|
|
|
if (includeEnhancedTrees)
|
|
AddEnhancedTrees(root, tile);
|
|
|
|
// Save as prefab
|
|
string prefabPath = $"{prefabOutputDir}/{tile.TileId}.prefab";
|
|
if (File.Exists(prefabPath))
|
|
AssetDatabase.DeleteAsset(prefabPath);
|
|
|
|
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
|
Debug.Log($"[GeoTilePrefabImporter] Created prefab: {prefabPath}");
|
|
|
|
// Cleanup temporary scene object
|
|
DestroyImmediate(root);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ApplyOrthoTexture(TerrainData terrainData, string tileId)
|
|
{
|
|
string orthoPath = Path.Combine(orthoDir, $"{tileId}.jpg").Replace("\\", "/");
|
|
if (!File.Exists(orthoPath))
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Ortho texture missing for {tileId}: {orthoPath}");
|
|
return;
|
|
}
|
|
|
|
EnsureOrthoImportSettings(orthoPath);
|
|
var orthoTex = AssetDatabase.LoadAssetAtPath<Texture2D>(orthoPath);
|
|
if (orthoTex == null)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Could not load ortho texture: {orthoPath}");
|
|
return;
|
|
}
|
|
|
|
// Create and save TerrainLayer as asset (required for prefab serialization)
|
|
var layer = new TerrainLayer
|
|
{
|
|
diffuseTexture = orthoTex,
|
|
tileSize = new Vector2(tileSizeMeters, tileSizeMeters),
|
|
tileOffset = Vector2.zero
|
|
};
|
|
|
|
string layerPath = $"{prefabOutputDir}/TerrainLayers/{tileId}_Layer.asset";
|
|
if (File.Exists(layerPath))
|
|
AssetDatabase.DeleteAsset(layerPath);
|
|
AssetDatabase.CreateAsset(layer, layerPath);
|
|
|
|
terrainData.terrainLayers = new[] { layer };
|
|
|
|
// Setup alphamap (single layer, full coverage)
|
|
terrainData.alphamapResolution = 16;
|
|
var alpha = new float[16, 16, 1];
|
|
for (int y = 0; y < 16; y++)
|
|
for (int x = 0; x < 16; x++)
|
|
alpha[y, x, 0] = 1f;
|
|
terrainData.SetAlphamaps(0, 0, alpha);
|
|
}
|
|
|
|
private void AddBuildings(GameObject root, TileMetadata tile)
|
|
{
|
|
string glbPath = Path.Combine(buildingsDir, $"{tile.TileId}.glb").Replace("\\", "/");
|
|
if (!File.Exists(glbPath))
|
|
return;
|
|
|
|
var buildingPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(glbPath);
|
|
if (buildingPrefab == null)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Could not load building GLB: {glbPath}");
|
|
return;
|
|
}
|
|
|
|
// 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)
|
|
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.isStatic = true;
|
|
}
|
|
|
|
private void AddTrees(GameObject root, TileMetadata tile)
|
|
{
|
|
if (!Directory.Exists(treesDir))
|
|
return;
|
|
|
|
// Look for chunk files or single file
|
|
var chunkFiles = Directory.GetFiles(treesDir, $"{tile.TileId}_*.glb");
|
|
if (chunkFiles.Length == 0)
|
|
{
|
|
string singlePath = Path.Combine(treesDir, $"{tile.TileId}.glb").Replace("\\", "/");
|
|
if (File.Exists(singlePath))
|
|
chunkFiles = new[] { singlePath };
|
|
else
|
|
return;
|
|
}
|
|
|
|
// 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)
|
|
// Rotate 180° around Y to correct coordinate system mismatch (Python negates Z),
|
|
// and offset X by tile_size to compensate for the rotation pivot
|
|
var treesContainer = new GameObject("Trees");
|
|
treesContainer.transform.SetParent(root.transform, false);
|
|
treesContainer.transform.localPosition = new Vector3(tileSizeMeters, -(float)tile.GlobalMin, 0f);
|
|
treesContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
|
|
treesContainer.isStatic = true;
|
|
|
|
foreach (var chunkPath in chunkFiles)
|
|
{
|
|
string assetPath = chunkPath.Replace("\\", "/");
|
|
var chunkPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
|
if (chunkPrefab == null)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Could not load tree chunk: {assetPath}");
|
|
continue;
|
|
}
|
|
|
|
var instance = PrefabUtility.InstantiatePrefab(chunkPrefab) as GameObject;
|
|
if (instance == null)
|
|
instance = Instantiate(chunkPrefab);
|
|
|
|
instance.name = Path.GetFileNameWithoutExtension(chunkPath);
|
|
instance.transform.SetParent(treesContainer.transform, false);
|
|
instance.transform.localPosition = Vector3.zero;
|
|
instance.isStatic = true;
|
|
}
|
|
}
|
|
|
|
private void AddFurniture(GameObject root, TileMetadata tile)
|
|
{
|
|
string csvPath = Path.Combine(furnitureDir, $"{tile.TileId}.csv").Replace("\\", "/");
|
|
if (!File.Exists(csvPath))
|
|
return;
|
|
|
|
var lines = File.ReadAllLines(csvPath);
|
|
if (lines.Length < 2)
|
|
return;
|
|
|
|
var ci = CultureInfo.InvariantCulture;
|
|
var header = lines[0].Split(',');
|
|
int idxXLocal = -1, idxYLocal = -1, idxZGround = -1, idxHeight = -1, idxType = -1;
|
|
|
|
for (int i = 0; i < header.Length; i++)
|
|
{
|
|
var col = header[i].Trim().ToLowerInvariant();
|
|
if (col == "x_local") idxXLocal = i;
|
|
else if (col == "y_local") idxYLocal = i;
|
|
else if (col == "z_ground") idxZGround = i;
|
|
else if (col == "height") idxHeight = i;
|
|
else if (col == "type") idxType = i;
|
|
}
|
|
|
|
if (idxXLocal < 0 || idxYLocal < 0 || idxType < 0)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Furniture CSV missing required columns: {csvPath}");
|
|
return;
|
|
}
|
|
|
|
var furnitureContainer = new GameObject("Furniture");
|
|
furnitureContainer.transform.SetParent(root.transform, false);
|
|
furnitureContainer.transform.localPosition = Vector3.zero;
|
|
furnitureContainer.isStatic = true;
|
|
|
|
float gmin = (float)tile.GlobalMin;
|
|
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
var parts = lines[i].Split(',');
|
|
if (parts.Length <= Math.Max(Math.Max(idxXLocal, idxYLocal), idxType))
|
|
continue;
|
|
|
|
try
|
|
{
|
|
float xLocal = float.Parse(parts[idxXLocal].Trim(), ci);
|
|
float yLocal = float.Parse(parts[idxYLocal].Trim(), ci);
|
|
float zGround = idxZGround >= 0 && idxZGround < parts.Length
|
|
? float.Parse(parts[idxZGround].Trim(), ci) : 0f;
|
|
float height = idxHeight >= 0 && idxHeight < parts.Length
|
|
? float.Parse(parts[idxHeight].Trim(), ci) : 1f;
|
|
string furnitureType = parts[idxType].Trim().ToLowerInvariant();
|
|
|
|
GameObject obj = CreateFurnitureObject(furnitureType, height, i);
|
|
obj.transform.SetParent(furnitureContainer.transform, false);
|
|
obj.transform.localPosition = new Vector3(xLocal, zGround - gmin, yLocal);
|
|
obj.isStatic = true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Failed to parse furniture line {i}: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private GameObject CreateFurnitureObject(string type, float height, int index)
|
|
{
|
|
GameObject prefabToUse = null;
|
|
Vector3 fallbackScale = Vector3.one;
|
|
|
|
switch (type)
|
|
{
|
|
case "lamp":
|
|
prefabToUse = lampPrefab;
|
|
fallbackScale = new Vector3(0.3f, height, 0.3f);
|
|
break;
|
|
case "bench":
|
|
prefabToUse = benchPrefab;
|
|
fallbackScale = new Vector3(1.5f, height, 0.5f);
|
|
break;
|
|
case "sign":
|
|
prefabToUse = signPrefab;
|
|
fallbackScale = new Vector3(0.1f, height, 0.5f);
|
|
break;
|
|
case "bollard":
|
|
prefabToUse = bollardPrefab;
|
|
fallbackScale = new Vector3(0.2f, height, 0.2f);
|
|
break;
|
|
default:
|
|
prefabToUse = defaultFurniturePrefab;
|
|
fallbackScale = new Vector3(0.5f, height, 0.5f);
|
|
break;
|
|
}
|
|
|
|
GameObject obj;
|
|
if (prefabToUse != null)
|
|
{
|
|
obj = PrefabUtility.InstantiatePrefab(prefabToUse) as GameObject ?? Instantiate(prefabToUse);
|
|
obj.name = $"{type}_{index}";
|
|
obj.transform.localScale = Vector3.one * height;
|
|
}
|
|
else
|
|
{
|
|
obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
obj.name = $"{type}_{index}";
|
|
obj.transform.localScale = fallbackScale;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
private void AddEnhancedTrees(GameObject root, TileMetadata tile)
|
|
{
|
|
string csvPath = Path.Combine(enhancedTreesDir, $"{tile.TileId}.csv").Replace("\\", "/");
|
|
if (!File.Exists(csvPath))
|
|
return;
|
|
|
|
var lines = File.ReadAllLines(csvPath);
|
|
if (lines.Length < 2)
|
|
return;
|
|
|
|
var ci = CultureInfo.InvariantCulture;
|
|
var header = lines[0].Split(',');
|
|
int idxXLocal = -1, idxYLocal = -1, idxZGround = -1, idxHeight = -1, idxRadius = -1;
|
|
int idxCanopyR = -1, idxCanopyG = -1, idxCanopyB = -1;
|
|
|
|
for (int i = 0; i < header.Length; i++)
|
|
{
|
|
var col = header[i].Trim().ToLowerInvariant();
|
|
if (col == "x_local") idxXLocal = i;
|
|
else if (col == "y_local") idxYLocal = i;
|
|
else if (col == "z_ground") idxZGround = i;
|
|
else if (col == "height") idxHeight = i;
|
|
else if (col == "radius") idxRadius = i;
|
|
else if (col == "canopy_r") idxCanopyR = i;
|
|
else if (col == "canopy_g") idxCanopyG = i;
|
|
else if (col == "canopy_b") idxCanopyB = i;
|
|
}
|
|
|
|
if (idxXLocal < 0 || idxYLocal < 0 || idxHeight < 0)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Enhanced trees CSV missing required columns: {csvPath}");
|
|
return;
|
|
}
|
|
|
|
var treesContainer = new GameObject("EnhancedTrees");
|
|
treesContainer.transform.SetParent(root.transform, false);
|
|
treesContainer.transform.localPosition = Vector3.zero;
|
|
treesContainer.isStatic = true;
|
|
|
|
float gmin = (float)tile.GlobalMin;
|
|
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
var parts = lines[i].Split(',');
|
|
if (parts.Length <= Math.Max(Math.Max(idxXLocal, idxYLocal), idxHeight))
|
|
continue;
|
|
|
|
try
|
|
{
|
|
float xLocal = float.Parse(parts[idxXLocal].Trim(), ci);
|
|
float yLocal = float.Parse(parts[idxYLocal].Trim(), ci);
|
|
float zGround = idxZGround >= 0 && idxZGround < parts.Length
|
|
? float.Parse(parts[idxZGround].Trim(), ci) : 0f;
|
|
float height = float.Parse(parts[idxHeight].Trim(), ci);
|
|
float radius = idxRadius >= 0 && idxRadius < parts.Length
|
|
? float.Parse(parts[idxRadius].Trim(), ci) : height * 0.25f;
|
|
|
|
int canopyR = 128, canopyG = 160, canopyB = 80;
|
|
if (idxCanopyR >= 0 && idxCanopyR < parts.Length)
|
|
canopyR = Mathf.Clamp(int.Parse(parts[idxCanopyR].Trim()), 0, 255);
|
|
if (idxCanopyG >= 0 && idxCanopyG < parts.Length)
|
|
canopyG = Mathf.Clamp(int.Parse(parts[idxCanopyG].Trim()), 0, 255);
|
|
if (idxCanopyB >= 0 && idxCanopyB < parts.Length)
|
|
canopyB = Mathf.Clamp(int.Parse(parts[idxCanopyB].Trim()), 0, 255);
|
|
|
|
Color canopyColor = new Color(canopyR / 255f, canopyG / 255f, canopyB / 255f);
|
|
|
|
GameObject treeObj = CreateEnhancedTree(height, radius, canopyColor, i);
|
|
treeObj.transform.SetParent(treesContainer.transform, false);
|
|
treeObj.transform.localPosition = new Vector3(xLocal, zGround - gmin, yLocal);
|
|
treeObj.isStatic = true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[GeoTilePrefabImporter] Failed to parse enhanced tree line {i}: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private GameObject CreateEnhancedTree(float height, float radius, Color canopyColor, int index)
|
|
{
|
|
if (treePrefab != null)
|
|
{
|
|
var treeObj = PrefabUtility.InstantiatePrefab(treePrefab) as GameObject ?? Instantiate(treePrefab);
|
|
treeObj.name = $"tree_{index}";
|
|
|
|
float scaleY = height / 10f;
|
|
float scaleXZ = radius * 2f / 5f;
|
|
treeObj.transform.localScale = new Vector3(scaleXZ, scaleY, scaleXZ);
|
|
|
|
var renderers = treeObj.GetComponentsInChildren<Renderer>();
|
|
foreach (var rend in renderers)
|
|
{
|
|
foreach (var mat in rend.sharedMaterials)
|
|
{
|
|
if (mat != null)
|
|
{
|
|
var matInst = new Material(mat);
|
|
matInst.color = canopyColor;
|
|
rend.material = matInst;
|
|
}
|
|
}
|
|
}
|
|
|
|
return treeObj;
|
|
}
|
|
|
|
// Create procedural placeholder
|
|
var tree = new GameObject($"tree_{index}");
|
|
|
|
var trunk = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
|
trunk.name = "trunk";
|
|
trunk.transform.SetParent(tree.transform, false);
|
|
trunk.transform.localPosition = new Vector3(0, height * 0.25f, 0);
|
|
trunk.transform.localScale = new Vector3(0.3f, height * 0.5f, 0.3f);
|
|
var trunkRend = trunk.GetComponent<Renderer>();
|
|
if (trunkRend != null)
|
|
{
|
|
var trunkMat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
|
|
trunkMat.color = new Color(0.4f, 0.25f, 0.15f);
|
|
trunkRend.material = trunkMat;
|
|
}
|
|
|
|
var canopy = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
|
canopy.name = "canopy";
|
|
canopy.transform.SetParent(tree.transform, false);
|
|
canopy.transform.localPosition = new Vector3(0, height * 0.7f, 0);
|
|
canopy.transform.localScale = new Vector3(radius * 2f, height * 0.5f, radius * 2f);
|
|
var canopyRend = canopy.GetComponent<Renderer>();
|
|
if (canopyRend != null)
|
|
{
|
|
var canopyMat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
|
|
canopyMat.color = canopyColor;
|
|
canopyRend.material = canopyMat;
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
#region Helper Methods
|
|
|
|
private static void EnsureDirectoryExists(string path)
|
|
{
|
|
if (!Directory.Exists(path))
|
|
{
|
|
Directory.CreateDirectory(path);
|
|
AssetDatabase.Refresh();
|
|
}
|
|
}
|
|
|
|
private static void EnsureHeightmapImportSettings(string assetPath)
|
|
{
|
|
var ti = (TextureImporter)AssetImporter.GetAtPath(assetPath);
|
|
if (ti == null) return;
|
|
|
|
bool changed = false;
|
|
|
|
if (!ti.isReadable) { ti.isReadable = true; changed = true; }
|
|
if (ti.sRGBTexture) { ti.sRGBTexture = false; changed = true; }
|
|
if (ti.textureCompression != TextureImporterCompression.Uncompressed)
|
|
{
|
|
ti.textureCompression = TextureImporterCompression.Uncompressed;
|
|
changed = true;
|
|
}
|
|
if (ti.npotScale != TextureImporterNPOTScale.None)
|
|
{
|
|
ti.npotScale = TextureImporterNPOTScale.None;
|
|
changed = true;
|
|
}
|
|
if (ti.textureType != TextureImporterType.SingleChannel)
|
|
{
|
|
ti.textureType = TextureImporterType.SingleChannel;
|
|
changed = true;
|
|
}
|
|
|
|
var ps = ti.GetDefaultPlatformTextureSettings();
|
|
if (ps.format != TextureImporterFormat.R16)
|
|
{
|
|
ps.format = TextureImporterFormat.R16;
|
|
ti.SetPlatformTextureSettings(ps);
|
|
changed = true;
|
|
}
|
|
|
|
changed |= EnsurePlatformR16(ti, "Standalone");
|
|
changed |= EnsurePlatformR16(ti, "Android");
|
|
changed |= EnsurePlatformR16(ti, "iPhone");
|
|
|
|
if (changed) ti.SaveAndReimport();
|
|
}
|
|
|
|
private static bool EnsurePlatformR16(TextureImporter ti, string platform)
|
|
{
|
|
var ps = ti.GetPlatformTextureSettings(platform);
|
|
|
|
bool changed = false;
|
|
if (ps.name != platform)
|
|
{
|
|
ps = new TextureImporterPlatformSettings
|
|
{
|
|
name = platform,
|
|
overridden = true,
|
|
format = TextureImporterFormat.R16,
|
|
textureCompression = TextureImporterCompression.Uncompressed,
|
|
};
|
|
ti.SetPlatformTextureSettings(ps);
|
|
return true;
|
|
}
|
|
|
|
if (!ps.overridden) { ps.overridden = true; changed = true; }
|
|
if (ps.format != TextureImporterFormat.R16) { ps.format = TextureImporterFormat.R16; changed = true; }
|
|
if (ps.textureCompression != TextureImporterCompression.Uncompressed)
|
|
{
|
|
ps.textureCompression = TextureImporterCompression.Uncompressed;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) ti.SetPlatformTextureSettings(ps);
|
|
return changed;
|
|
}
|
|
|
|
private static void EnsureOrthoImportSettings(string assetPath)
|
|
{
|
|
var ti = (TextureImporter)AssetImporter.GetAtPath(assetPath);
|
|
if (ti == null) return;
|
|
|
|
bool changed = false;
|
|
if (!ti.isReadable) { ti.isReadable = true; changed = true; }
|
|
if (!ti.sRGBTexture) { ti.sRGBTexture = true; changed = true; }
|
|
if (ti.npotScale != TextureImporterNPOTScale.None) { ti.npotScale = TextureImporterNPOTScale.None; changed = true; }
|
|
if (ti.wrapMode != TextureWrapMode.Clamp) { ti.wrapMode = TextureWrapMode.Clamp; changed = true; }
|
|
if (ti.textureCompression != TextureImporterCompression.Uncompressed)
|
|
{
|
|
ti.textureCompression = TextureImporterCompression.Uncompressed;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) ti.SaveAndReimport();
|
|
}
|
|
|
|
private static string NormalizeHeader(string s)
|
|
=> (s ?? "").Trim().ToLowerInvariant();
|
|
|
|
private static Dictionary<string, int> BuildHeaderMap(string headerLine)
|
|
{
|
|
var map = new Dictionary<string, int>();
|
|
var cols = headerLine.Split(',');
|
|
for (int i = 0; i < cols.Length; i++)
|
|
{
|
|
var key = NormalizeHeader(cols[i]);
|
|
if (string.IsNullOrEmpty(key)) continue;
|
|
if (!map.ContainsKey(key))
|
|
map[key] = i;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private static bool HasAll(Dictionary<string, int> map, params string[] required)
|
|
{
|
|
foreach (var r in required)
|
|
if (!map.ContainsKey(NormalizeHeader(r)))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Component attached to tile prefab roots to store geo metadata.
|
|
/// Useful for positioning prefabs in scene or querying tile info at runtime.
|
|
/// </summary>
|
|
public class GeoTileMetadata : MonoBehaviour
|
|
{
|
|
public string tileId;
|
|
public double xmin;
|
|
public double ymin;
|
|
public double globalMin;
|
|
public double globalMax;
|
|
|
|
/// <summary>
|
|
/// Returns the world position this tile should be placed at, given a global origin.
|
|
/// </summary>
|
|
public Vector3 GetWorldPosition(double originX, double originY)
|
|
{
|
|
return new Vector3(
|
|
(float)(xmin - originX),
|
|
(float)globalMin,
|
|
(float)(ymin - originY)
|
|
);
|
|
}
|
|
}
|