Files
GeoSusGame/Assets/Scripts/MapRenderer.cs
2025-11-15 17:38:49 +01:00

509 lines
17 KiB
C#

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 string overpassUrl = "https://mapz.honzuvkod.dev/api/interpreter";
public float queryRadiusMeters = 200f; // radius around lat/lon to query
[Header("Location (lat, lon)")]
public double latitude; // example Prague
public double longitude;
[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
[Header("Misc")]
public float metersPerUnit = 1f; // scale: 1 unit = 1 meter
public bool autoStart = true;
// Internal storage
Dictionary<long, Vector2> nodes = new Dictionary<long, Vector2>(); // id -> latlon
void Start()
{
if (autoStart)
StartCoroutine(GenerateForLocation(latitude, longitude));
}
// Public entry for other scripts
public void StartGenerating(double lat, double lon)
{
latitude = lat; longitude = lon;
StartCoroutine(GenerateForLocation(lat, lon));
}
IEnumerator GenerateForLocation(double lat, double lon)
{
ClearChildren();
// compute bbox from radius
float degPerMeter = 1f / 111000f; // approximate
double delta = queryRadiusMeters * degPerMeter;
double south = lat - delta;
double north = lat + delta;
double west = lon - delta;
double east = lon + delta;
string q = $"[out:xml][timeout:25];(way[\"building\"]({south.ToString().Replace(",",".")},{west.ToString().Replace(",", ".")},{north.ToString().Replace(",", ".")},{east.ToString().Replace(",", ".")});way[\"highway\"]({south.ToString().Replace(",", ".")},{west.ToString().Replace(",", ".")},{north.ToString().Replace(",", ".")},{east.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);
// create separate GameObjects for buildings and roads
GameObject buildingsRoot = new GameObject("Buildings");
buildingsRoot.transform.parent = this.transform;
GameObject roadsRoot = new GameObject("Roads");
roadsRoot.transform.parent = this.transform;
// iterate parsed ways
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()
{
// remove existing generated children
List<GameObject> toDestroy = new List<GameObject>();
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<long> nodeRefs = new List<long>();
public Dictionary<string, string> tags = new Dictionary<string, string>();
}
List<Way> parsedWays = new List<Way>();
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<Vector3> poly = new List<Vector3>();
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<MeshFilter>();
MeshRenderer mr = go.AddComponent<MeshRenderer>();
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<Vector2> poly2D = new List<Vector2>();
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<int> 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<Vector3> verts = new List<Vector3>();
List<int> triangles = new List<int>();
List<Vector2> uvs = new List<Vector2>();
// 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<Vector3> pts = new List<Vector3>();
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 = 10f;
else if (h == "primary") width = 8f;
else if (h == "secondary") width = 6f;
else if (h == "tertiary") width = 5f;
else width = defaultRoadWidth;
}
GameObject go = new GameObject("Road_" + w.id);
MeshFilter mf = go.AddComponent<MeshFilter>();
MeshRenderer mr = go.AddComponent<MeshRenderer>();
mr.material = roadMaterial;
Mesh mesh = new Mesh();
mesh.name = "RoadMesh_" + w.id;
List<Vector3> verts = new List<Vector3>();
List<int> tris = new List<int>();
List<Vector2> uvs = new List<Vector2>();
// 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<int> Triangulate(List<Vector2> poly)
{
List<int> indices = new List<int>();
int n = poly.Count;
if (n < 3) return indices;
List<int> V = new List<int>();
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
}