Sunday Experiment - Using OpenStreetMap Data to Build Real Roads in When Pigs Fly

A while back I wrote a post on using Voronoi noise to build procedural roads.  Now that I've transitioned to a real-world map, that solution is less than ideal.  The roads in area I've chosen to recreate are pretty simple (and there aren't many in the first place).  If I stuck to just the main roads, I could probably add them all by hand.  As a solo developer, though, time is precious.  This morning I started experimenting with data from OpenStreetMap (which is provided under the ODbL license). 

Straight from OpenStreetMap, the area I wanted to work with provided a lot of data (the first json file I got was 11.2mb). Mixed in with the road data was information about points of interest, traffic lights, stop signs... even benches on the side of the road.  In order to filter out all this unnecessary data, I moved from the OSM website to a tool called Overpass Turbo.  This is a GREAT site that allows you to build queries very simply, then spits the data back to you in a variety of formats. Using the query wizard, I filtered out everything but "highways", which include roads, parking lots, and even hiking trails.  This was still a lot of data, but much more manageable. To keep things nice and easy for this experiment, I downsized further and focused only on the area immediately surrounding Jenny Lake.  I downloaded both the raw OSM data as well as the geoJSON version (this time only 386kb).  Here is what the data looks like visualized on Overpass Turbo.

And here's a peak at the raw JSON data.

As you can see, the JSON data contains a lot of information that will be useful, such as whether the road is a highway, a neighborhood street, or a parking lot.  Down the line I'll be able to use that to match the right road model.  For today, though, everything from a highway to a hiking path is going to look exactly the same.

To get the data into Unity I used SimpleJSON available on the Unify Community Wiki.  To keep things quick, I reused the mesh generation code from my previous Voronoi road generator.  It very simply builds roads based on line segments.  For this reason, I opted to use the geoJSON over the raw OSM data.  The OSM data lists nodes (with coordinates), then "ways" (roads built up from node to node).  The geoJSON version more simply just lists the roads and their respective coordinates.  Here is the code:


using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using SimpleJSON;
 
public struct LineSegment
{
    public Vector3 p0;
    public Vector3 p1;
}
 
public class RoadBuilder : MonoBehaviour 
{
    public TextAsset geoJSONText;
    public float roadHalfWidth;
    public Color roadColor;
 
    void Start()
    {
        List<LineSegment> segments = new List<LineSegment>();
 
        string geoJSONstring = geoJSONText.text;
        var N = JSON.Parse(geoJSONstring);
 
        int numFeatures = N["features"].Count;
        double x,z;
        Vector3 coord = Vector3.zero;
        Vector3 prevCoord = Vector3.zero;
        LineSegment segment = new LineSegment();
 
        //find bounds
        double maxX = -1000d;
        double minX = 1000d;
        double maxZ = -1000d;
        double minZ = 1000d;
        for(int feature = 0; feature < numFeatures; feature++)
        {
            if(N["features"][feature]["geometry"]["type"].Value != "LineString")
                continue;
 
            int numCoordinates = N["features"][feature]["geometry"]["coordinates"].Count;
            for(int coordinate = 0; coordinate < numCoordinates; coordinate++)
            {
                x = N["features"][feature]["geometry"]["coordinates"][coordinate][0].AsDouble;
                z = N["features"][feature]["geometry"]["coordinates"][coordinate][1].AsDouble;
                if(x > maxX)
                    maxX = x;
                if(x < minX)
                    minX = x;
                if(z > maxZ)
                    maxZ = z;
                if(z < minZ)
                    minZ = z;
            }
        }
        double centerX = minX + (maxX - minX) * 0.5d;
        double centerZ = minZ + (maxZ - minZ) * 0.5d;
 
        //Build line segments
        for(int feature = 0; feature < numFeatures; feature++)
        {
            if(N["features"][feature]["geometry"]["type"].Value != "LineString")
                continue;
 
            int numCoordinates = N["features"][feature]["geometry"]["coordinates"].Count;
 
            //Put first coordinate in prevWorldCoord
            x = N["features"][feature]["geometry"]["coordinates"][0][0].AsDouble;
            z = N["features"][feature]["geometry"]["coordinates"][0][1].AsDouble;
            //convert to world space
            x -= centerX;
            x *= 80206d;
            z -= centerZ;
            z *= 111112d;
 
            prevCoord.x = (float)x;
            prevCoord.z = (float)z;
 
            for(int coordinate = 1; coordinate < numCoordinates; coordinate++)
            {
                //Read coordinates
                x = N["features"][feature]["geometry"]["coordinates"][coordinate][0].AsDouble;
                z = N["features"][feature]["geometry"]["coordinates"][coordinate][1].AsDouble;
 
                //Convert to world space
                x -= centerX;
                x *= 80206d;
                z -= centerZ;
                z *= 111112d;
 
                coord.x = (float)x;
                coord.z = (float)z;
 
                //Add segment to list
                segment.p0 = prevCoord;
                segment.p1 = coord;
                segments.Add(segment);
 
                prevCoord = coord;
            }
        }
 
 
        //Build mesh
        List<Vector3> verts = new List<Vector3>();
        List<int> tris = new List<int>();
        List<Vector3> normals = new List<Vector3>();
        List<Color> col = new List<Color>();
        
        int triIndex = 0;
        
        for(int i = 0; i < segments.Count; i++)
        {
            Vector3 p0 = segments[i].p0;
            Vector3 p1 = segments[i].p1;
            Vector3 perpendicular = Vector3.Cross(Vector3.up, p1 - p0).normalized;
            
            verts.Add(p0 - (perpendicular * roadHalfWidth));
            verts.Add(p0 + (perpendicular * roadHalfWidth));
            verts.Add(p1 - (perpendicular * roadHalfWidth));
            verts.Add(p1 + (perpendicular * roadHalfWidth));
            
            tris.Add(triIndex);
            tris.Add(triIndex + 2);
            tris.Add(triIndex + 1);
            
            tris.Add(triIndex + 2);
            tris.Add(triIndex + 3);
            tris.Add(triIndex + 1);
            
            triIndex += 4;
            
            for(int y = 0; y < 4; y++)
            {
                normals.Add(Vector3.up);
                col.Add(roadColor);
            }
        }
        
        MeshFilter filter = GetComponent<MeshFilter>();
        Mesh mesh = filter.sharedMesh;
        if(mesh == null)
        {
            mesh = new Mesh();
            mesh.name = "roads";
            filter.sharedMesh = mesh;
        }
        mesh.vertices = verts.ToArray();
        mesh.triangles = tris.ToArray();
        mesh.normals = normals.ToArray();
        mesh.colors = col.ToArray();
        mesh.RecalculateBounds();
        mesh.UploadMeshData(true);
    }    
}

Note: this code was written hastily early on a Sunday morning.  It may point you in the right direction, but it certainly isn't ideal.  In addition to being unoptimized, it currently creates perfectly flat roads with no intersections and gaps in the mesh at turns.  Use at your own risk.

Here is what the road looks like in-game:

Not too bad!  I'm encouraged by this progress, but there is still a lot of work to do.  For starters, I need to improve the mesh generation in a number of ways.  Different models for different types of roads, intersections, and matching road height to the terrain will all be more involved than actually getting the data into the game.  While I chose geoJSON for this experiment, I think I will be using the raw OSM data going forward.  Representing the roads as a system of nodes and ways will make detecting and correctly modeling intersections much easier.  Its also a start on a routing system for potentially adding AI traffic in the future!