// 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("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(assetPath); if (prefab == null) continue; var meta = prefab.GetComponent(); 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 ParseTilesCsv() { var tiles = new List(); 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(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(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 (ensures prefab contains the terrain asset) var root = Terrain.CreateTerrainGameObject(terrainData); root.name = tile.TileId; var terrain = root.GetComponent(); terrain.drawInstanced = true; // Store metadata as component for later use var metadata = root.AddComponent(); 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(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(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.transform.localRotation = Quaternion.Euler(0f, 180f, 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) var treesContainer = new GameObject("Trees"); treesContainer.transform.SetParent(root.transform, false); treesContainer.transform.localPosition = new Vector3(0f, -(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(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(); 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(); 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(); 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 BuildHeaderMap(string headerLine) { var map = new Dictionary(); 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 map, params string[] required) { foreach (var r in required) if (!map.ContainsKey(NormalizeHeader(r))) return false; return true; } #endregion }