using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Xml; using UnityEngine; using UnityEngine.Networking; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class MapRenderer : MonoBehaviour { [Header("Overpass settings")] public const string overpassUrl = "https://mapz.honzuvkod.dev/api/interpreter"; public float queryRadiusMeters = 200f; // radius around lat/lon to query [Header("Location (lat, lon)")] public GPSManager gpsManager; private double latitude = 50.7727878; private double longitude = 15.0718625; [Header("Building settings")] public Material buildingMaterial; public float defaultFloorHeight = 3.0f; // meters per level public float defaultBuildingHeight = 6.0f; // if no tags [Header("Road settings")] public Material roadMaterial; public float defaultRoadWidth = 4.0f; // meters public float motorwayWidth = 10.0f; public float primaryWidth = 8.0f; public float secondaryWidth = 6.0f; public float tertiaryWidth = 5.0f; [Header("Misc")] public float metersPerUnit = 1f; // scale: 1 unit = 1 meter [Header("Storage")] Dictionary nodes = new Dictionary(); // id -> latlon List parsedWays = new List(); void Start() { StartCoroutine(RenderMap()); } IEnumerator RenderMap() { ClearChildren(); double[] GPS = gpsManager.GetLastCoords(); latitude = GPS[0]; longitude = GPS[1]; string q = $"[out:xml][timeout:90];(way[\"building\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")});way[\"highway\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")}););(._;>;);out body;"; WWWForm form = new WWWForm(); form.AddField("data", q); using (UnityWebRequest www = UnityWebRequest.Post(overpassUrl, form)) { www.downloadHandler = new DownloadHandlerBuffer(); yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.LogError("Overpass request failed: " + www.error); yield break; } string xml = www.downloadHandler.text; ParseOverpassXml(xml); GameObject buildingsRoot = new GameObject("Buildings"); buildingsRoot.transform.parent = this.transform; GameObject roadsRoot = new GameObject("Roads"); roadsRoot.transform.parent = this.transform; foreach (var w in parsedWays) { if (w.tags.ContainsKey("building")) { GameObject b = BuildBuildingMesh(w); b.transform.parent = buildingsRoot.transform; } else if (w.tags.ContainsKey("highway")) { GameObject r = BuildRoadMesh(w); r.transform.parent = roadsRoot.transform; } } Debug.Log("Map generation complete: " + parsedWays.Count + " ways, " + nodes.Count + " nodes."); } } void ClearChildren() { List toDestroy = new List(); foreach (Transform t in transform) toDestroy.Add(t.gameObject); foreach (var g in toDestroy) DestroyImmediate(g); } #region Overpass XML parsing class Way { public long id; public List nodeRefs = new List(); public Dictionary tags = new Dictionary(); } void ParseOverpassXml(string xmlText) { nodes.Clear(); parsedWays.Clear(); XmlDocument doc = new XmlDocument(); doc.LoadXml(xmlText); XmlNode osm = doc.SelectSingleNode("/osm"); if (osm == null) return; // parse nodes foreach (XmlNode node in osm.SelectNodes("node")) { long id = long.Parse(node.Attributes["id"].Value, CultureInfo.InvariantCulture); double lat = double.Parse(node.Attributes["lat"].Value, CultureInfo.InvariantCulture); double lon = double.Parse(node.Attributes["lon"].Value, CultureInfo.InvariantCulture); nodes[id] = new Vector2((float)lat, (float)lon); } // parse ways foreach (XmlNode wayNode in osm.SelectNodes("way")) { Way w = new Way(); w.id = long.Parse(wayNode.Attributes["id"].Value, CultureInfo.InvariantCulture); foreach (XmlNode child in wayNode.ChildNodes) { if (child.Name == "nd") { long r = long.Parse(child.Attributes["ref"].Value, CultureInfo.InvariantCulture); w.nodeRefs.Add(r); } else if (child.Name == "tag") { string k = child.Attributes["k"].Value; string v = child.Attributes["v"].Value; w.tags[k] = v; } } parsedWays.Add(w); } } #endregion #region Utilities: latlon to local meters // Convert latitude/longitude to local XY meters relative to center point Vector3 LatLonToLocal(double lat, double lon) { // Use simple equirectangular projection around center (latitude, longitude) double lat0 = latitude; double lon0 = longitude; double dLat = (lat - lat0) * Mathf.Deg2Rad; double dLon = (lon - lon0) * Mathf.Deg2Rad; double R = 6378137.0; // Earth radius in meters double x = R * dLon * Math.Cos(lat0 * Mathf.Deg2Rad); double y = R * dLat; return new Vector3((float)x / metersPerUnit, 0f, (float)y / metersPerUnit); } Vector3 NodeIdToLocal(long nodeId) { if (!nodes.ContainsKey(nodeId)) return Vector3.zero; Vector2 latlon = nodes[nodeId]; return LatLonToLocal(latlon.x, latlon.y); } #endregion #region Mesh builders GameObject BuildBuildingMesh(Way w) { // gather polygon points List poly = new List(); foreach (var id in w.nodeRefs) { Vector3 p = NodeIdToLocal(id); poly.Add(p); } // ensure closed if (poly.Count < 3) return null; if ((poly[0] - poly[poly.Count - 1]).sqrMagnitude > 0.0001f) poly.Add(poly[0]); // determine height float height = defaultBuildingHeight; if (w.tags.ContainsKey("height")) { if (TryParseHeight(w.tags["height"], out float h)) height = h; } else if (w.tags.ContainsKey("building:levels")) { if (float.TryParse(w.tags["building:levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels)) height = Mathf.Max(0.5f, levels * defaultFloorHeight); } else if (w.tags.ContainsKey("levels")) { if (float.TryParse(w.tags["levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels)) height = Mathf.Max(0.5f, levels * defaultFloorHeight); } // create GameObject GameObject go = new GameObject("Building_" + w.id); MeshFilter mf = go.AddComponent(); MeshRenderer mr = go.AddComponent(); mr.material = buildingMaterial; // generate mesh: roof (triangulated polygon) + walls (extruded quads) Mesh mesh = new Mesh(); mesh.name = "BuildingMesh_" + w.id; // Convert poly to 2D points (XZ plane) List poly2D = new List(); for (int i = 0; i < poly.Count - 1; i++) // omit last repeated point poly2D.Add(new Vector2(poly[i].x, poly[i].z)); // triangulate roof List roofTris = Triangulate(poly2D); if (roofTris == null || roofTris.Count == 0) { Debug.LogWarning("Triangulation failed for building " + w.id); return go; } // Build vertices: roof vertices at y=height, walls vertices (2 per poly vertex) int n = poly2D.Count; // Build vertices and triangles with NO SHARED VERTICES (flat shading) List verts = new List(); List triangles = new List(); List uvs = new List(); // Roof triangles - each triangle gets its own vertices for (int i = 0; i < roofTris.Count; i += 3) { int idx0 = roofTris[i]; int idx1 = roofTris[i + 1]; int idx2 = roofTris[i + 2]; Vector2 p0 = poly2D[idx0]; Vector2 p1 = poly2D[idx1]; Vector2 p2 = poly2D[idx2]; int baseIdx = verts.Count; verts.Add(new Vector3(p0.x, height / metersPerUnit, p0.y)); verts.Add(new Vector3(p1.x, height / metersPerUnit, p1.y)); verts.Add(new Vector3(p2.x, height / metersPerUnit, p2.y)); triangles.Add(baseIdx); triangles.Add(baseIdx + 1); triangles.Add(baseIdx + 2); uvs.Add(new Vector2(p0.x, p0.y)); uvs.Add(new Vector2(p1.x, p1.y)); uvs.Add(new Vector2(p2.x, p2.y)); } // Walls - each quad gets its own 4 vertices for (int i = 0; i < n; i++) { int iNext = (i + 1) % n; Vector2 p0 = poly2D[i]; Vector2 p1 = poly2D[iNext]; int baseIdx = verts.Count; verts.Add(new Vector3(p0.x, height / metersPerUnit, p0.y)); // top left verts.Add(new Vector3(p0.x, 0, p0.y)); // bottom left verts.Add(new Vector3(p1.x, 0, p1.y)); // bottom right verts.Add(new Vector3(p1.x, height / metersPerUnit, p1.y)); // top right triangles.Add(baseIdx); triangles.Add(baseIdx + 1); triangles.Add(baseIdx + 2); triangles.Add(baseIdx); triangles.Add(baseIdx + 2); triangles.Add(baseIdx + 3); uvs.Add(new Vector2(0, 1)); uvs.Add(new Vector2(0, 0)); uvs.Add(new Vector2(1, 0)); uvs.Add(new Vector2(1, 1)); } mesh.SetVertices(verts); mesh.SetTriangles(triangles, 0); mesh.SetUVs(0, uvs); mesh.RecalculateNormals(); mesh.RecalculateBounds(); mf.mesh = mesh; // Center object (using first roof vertex as reference) Vector3 centroid = Vector3.zero; for (int i = 0; i < roofTris.Count; i += 3) { centroid += verts[i]; } centroid /= (roofTris.Count / 3); go.transform.position = centroid * -1f; // Move the roof/walls vertices back to local space Vector3[] adjustedVerts = mesh.vertices; for (int i = 0; i < adjustedVerts.Length; i++) adjustedVerts[i] += centroid; mesh.vertices = adjustedVerts; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return go; } GameObject BuildRoadMesh(Way w) { // build polyline List pts = new List(); foreach (var id in w.nodeRefs) pts.Add(NodeIdToLocal(id)); if (pts.Count < 2) return null; float width = defaultRoadWidth; if (w.tags.ContainsKey("width") && float.TryParse(w.tags["width"], NumberStyles.Float, CultureInfo.InvariantCulture, out float wv)) width = wv; else if (w.tags.ContainsKey("highway")) { // simple heuristic string h = w.tags["highway"]; if (h == "motorway") width = motorwayWidth; else if (h == "primary") width = primaryWidth; else if (h == "secondary") width = secondaryWidth; else if (h == "tertiary") width = tertiaryWidth; else width = defaultRoadWidth; } GameObject go = new GameObject("Road_" + w.id); MeshFilter mf = go.AddComponent(); MeshRenderer mr = go.AddComponent(); mr.material = roadMaterial; Mesh mesh = new Mesh(); mesh.name = "RoadMesh_" + w.id; List verts = new List(); List tris = new List(); List uvs = new List(); // build quad strip for (int i = 0; i < pts.Count; i++) { Vector3 p = pts[i]; Vector3 dir; if (i == 0) dir = (pts[i + 1] - p).normalized; else if (i == pts.Count - 1) dir = (p - pts[i - 1]).normalized; else dir = (pts[i + 1] - pts[i - 1]).normalized; Vector3 normal = Vector3.Cross(dir, Vector3.up).normalized; Vector3 left = p + normal * (width * 0.5f / metersPerUnit); Vector3 right = p - normal * (width * 0.5f / metersPerUnit); verts.Add(left); verts.Add(right); uvs.Add(new Vector2(0, i)); uvs.Add(new Vector2(1, i)); if (i > 0) { int baseIdx = verts.Count - 4; tris.Add(baseIdx + 0); tris.Add(baseIdx + 2); tris.Add(baseIdx + 1); tris.Add(baseIdx + 1); tris.Add(baseIdx + 2); tris.Add(baseIdx + 3); } } mesh.SetVertices(verts); mesh.SetTriangles(tris, 0); mesh.SetUVs(0, uvs); mesh.RecalculateNormals(); mesh.RecalculateBounds(); mf.mesh = mesh; go.transform.position = Vector3.zero; return go; } #endregion #region Helpers bool TryParseHeight(string s, out float meters) { // try to parse heights like "12", "12.5m", "40 ft" s = s.Trim(); meters = 0f; if (s.EndsWith("m")) s = s.Substring(0, s.Length - 1).Trim(); if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { meters = v; return true; } // fallback: try to extract number StringBuilder num = new StringBuilder(); foreach (char c in s) if ((c >= '0' && c <= '9') || c == '.' || c == ',') num.Append(c == ',' ? '.' : c); if (num.Length > 0 && float.TryParse(num.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out v)) { meters = v; return true; } return false; } // Basic ear clipping triangulation for simple polygons (2D) List Triangulate(List poly) { List indices = new List(); int n = poly.Count; if (n < 3) return indices; List V = new List(); for (int i = 0; i < n; i++) V.Add(i); int guard = 0; while (V.Count > 3 && guard < 10000) { bool earFound = false; for (int i = 0; i < V.Count; i++) { int prev = V[(i - 1 + V.Count) % V.Count]; int curr = V[i]; int next = V[(i + 1) % V.Count]; Vector2 a = poly[prev]; Vector2 b = poly[curr]; Vector2 c = poly[next]; if (!IsConvex(a, b, c)) continue; bool hasPointInside = false; for (int j = 0; j < V.Count; j++) { int vi = V[j]; if (vi == prev || vi == curr || vi == next) continue; if (PointInTriangle(poly[vi], a, b, c)) { hasPointInside = true; break; } } if (hasPointInside) continue; // ear found indices.Add(prev); indices.Add(curr); indices.Add(next); V.RemoveAt(i); earFound = true; break; } if (!earFound) break; guard++; } if (V.Count == 3) { indices.Add(V[0]); indices.Add(V[1]); indices.Add(V[2]); } return indices; } bool IsConvex(Vector2 a, Vector2 b, Vector2 c) { return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0f; // changed > to } bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) { float area = TriangleArea(a, b, c); float area1 = TriangleArea(p, b, c); float area2 = TriangleArea(a, p, c); float area3 = TriangleArea(a, b, p); return Mathf.Abs(area - (area1 + area2 + area3)) < 1e-3f; } float TriangleArea(Vector2 a, Vector2 b, Vector2 c) { return Mathf.Abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) * 0.5f); } #endregion }