Skip to content
Snippets Groups Projects
VectorTile.cs 20.5 KiB
Newer Older
using Mapbox.Vector.Tile;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
Knuiman, Bart's avatar
Knuiman, Bart committed
using UnityEngine.Networking;

    internal class QuadTreeNode
    {
        internal Vector2 min, max;
        internal QuadTreeNode[] children;
        internal List<(int, int, int)> triangles; // Layer, Feature, Triangle index.
        internal int depth;
    }

    // Always multiple of 3. Each 3 form a triangle.
Knuiman, Bart's avatar
Knuiman, Bart committed
    public class TriangulatedPolygon
Knuiman, Bart's avatar
Knuiman, Bart committed
        public List<Vector2> mins;
        public List<Vector2> maxs;
        public List<float>   denoms;

        public void OptimizeForIsPointInTriangle()
        {
            if (vertices == null) return; // Non poly layers have this.
Knuiman, Bart's avatar
Knuiman, Bart committed

            mins   = new List<Vector2>( vertices.Count / 3 );
            maxs   = new List<Vector2>( vertices.Count / 3 );
            denoms = new List<float>( vertices.Count / 3 );

Knuiman, Bart's avatar
Knuiman, Bart committed
            {
                float minx = float.MaxValue;
                float miny = float.MaxValue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                if (vertices[i+0].x < minx) minx = vertices[i].x;
                if (vertices[i+0].y < miny) miny = vertices[i].y;
                if (vertices[i+1].x < minx) minx = vertices[i+1].x;
                if (vertices[i+1].y < miny) miny = vertices[i+1].y;
                if (vertices[i+2].x < minx) minx = vertices[i+2].x;
                if (vertices[i+2].y < miny) miny = vertices[i+2].y;
                mins.Add( new Vector2( minx, miny ) );

                float maxx = float.MinValue;
                float maxy = float.MinValue;
                if (vertices[i+0].x > maxx) maxx = vertices[i].x;
                if (vertices[i+0].y > maxy) maxy = vertices[i].y;
                if (vertices[i+1].x > maxx) maxx = vertices[i+1].x;
                if (vertices[i+1].y > maxy) maxy = vertices[i+1].y;
                if (vertices[i+2].x > maxx) maxx = vertices[i+2].x;
                if (vertices[i+2].y > maxy) maxy = vertices[i+2].y;
                maxs.Add( new Vector2( maxx, maxy ) );

                // float denominator = ((vertex2.y - vertex3.y) * (vertex1.x - vertex3.x) + (vertex3.x - vertex2.x) * (vertex1.y - vertex3.y));
                float denominator = ((vertices[i+1].y - vertices[i+2].y) * (vertices[i].x - vertices[i+2].x) + (vertices[i+2].x - vertices[i+1].x) * (vertices[i].y - vertices[i+2].y));
                denominator = 1.0f / denominator;
                denoms.Add( denominator );
Knuiman, Bart's avatar
Knuiman, Bart committed
            }
        }
Knuiman, Bart's avatar
Knuiman, Bart committed
    public class VectorTile
    {
Knuiman, Bart's avatar
Knuiman, Bart committed
        public bool DownloadStarted => started;
        public bool Valid => valid;
        public bool Finished => finished;
        public bool Triangulated => triangulated;
Knuiman, Bart's avatar
Knuiman, Bart committed
        private bool started;
        private bool valid;
        private bool finished;
        private bool triangulated;
Knuiman, Bart's avatar
Knuiman, Bart committed
        private bool layersIdentified;
        private Task parseTileTask;
        private List<VectorTileLayer> layers;
        private List<List<TriangulatedPolygon>> polygonLayers;
        private QuadTreeNode root;
Knuiman, Bart's avatar
Knuiman, Bart committed
        internal UnityWebRequest request;

        public void StartDownload()
        {
Knuiman, Bart's avatar
Knuiman, Bart committed
            request.SendWebRequest();
            started = true;
        }
        {
            if (finished)
                return true;

            if (!request.isDone)
                return false;

Knuiman, Bart's avatar
Knuiman, Bart committed
            if (request.result == UnityWebRequest.Result.Success)
            {
                if (parseTileTask == null)
                {
Knuiman, Bart's avatar
Knuiman, Bart committed
                    var data = request.downloadHandler.data; // call on mainthread.
                    parseTileTask = Task.Run( () =>
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        var stream = new MemoryStream( data );
                        layers = VectorTileParser.Parse( stream );
                    } );
                }
                else if (parseTileTask.IsCompleted)
                {
                    valid = parseTileTask.IsCompletedSuccessfully;
                    finished = true;
                    parseTileTask = null;
                }
            }

            return finished;
        }

        // Number of failed polys to triangulate are returned.
        public int Triangulate()
                return 0;
            int numFailedPolys = 0;
            polygonLayers = new List<List<TriangulatedPolygon>>();
            List<double> vertices = new List<double>();
            List<int> holeIndices = new List<int>();
            {
                var features = layers[i].VectorTileFeatures;
                var polygons = new List<TriangulatedPolygon>();
Knuiman, Bart's avatar
Knuiman, Bart committed
                    polygons.Add( new TriangulatedPolygon() ); // Add empty to ensure list(layer) of lists(features) match.
                    if (features[j].GeometryType != Tile.GeomType.Polygon)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    {
                        continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                    }
                    vertices.Clear();
                    holeIndices.Clear();
                    var rings = features[j].Geometry;
                    for (int k = 0;k < rings.Count;k++)
                    {
                        if (k != 0) // if is not first ring, this is a hole
                        {
                            holeIndices.Add( vertices.Count / 2 );
                        }
                        var ring = rings[k];
                        for (int q = 0;q < ring.Count;q++)
                        {
                            vertices.Add( ring[q].X );
                            vertices.Add( ring[q].Y );
                        }
                    }
                    try
                    {
                        var indices = EarcutNet.Earcut.Tessellate( vertices, holeIndices );
                        TriangulatedPolygon poly = new TriangulatedPolygon();
                        poly.vertices = new List<Vector2>( indices.Count );
                        {
                            double x = vertices[indices[k]*2];
                            double y = vertices[indices[k]*2+1];
                            poly.vertices.Add( new Vector2( (float)x, (float)y ) );
                        }
Knuiman, Bart's avatar
Knuiman, Bart committed
                        polygons[polygons.Count-1] = poly;
                        UnityEngine.Debug.Assert( poly.vertices.Count % 3 == 0 );
                    }
                    catch (Exception)
                    {
                        numFailedPolys++;
                    }
                }
                polygonLayers.Add( polygons );
            }
            triangulated = true;
            return numFailedPolys;
        }

Knuiman, Bart's avatar
Knuiman, Bart committed
        public void OptimizeForPointIsInsideTriangle()
        {
Knuiman, Bart's avatar
Knuiman, Bart committed
            try
Knuiman, Bart's avatar
Knuiman, Bart committed
                Stopwatch sw = new Stopwatch();
                sw.Restart();
                for (int i = 0;i < polygonLayers.Count;i++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    for (int j = 0;j < polygonLayers[i].Count;j++)
                    {
                        polygonLayers[i][j].OptimizeForIsPointInTriangle();
                    }
Knuiman, Bart's avatar
Knuiman, Bart committed
                // Generate quadtree
                List<QuadTreeNode> stack = new List<QuadTreeNode>();
                root = new QuadTreeNode();
                root.triangles = new List<(int, int, int)>();
                root.min = new Vector2( 0, 0 );
                root.max = new Vector2( 4096, 4096 ); // TODO 4096 should be max of all layer extents.
                for (int l = 0;l < polygonLayers.Count;l++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    var layer = layers[l];
                    for (int f = 0;f < layer.VectorTileFeatures.Count;f++)
                    {
                        var feature = layer.VectorTileFeatures[f];
Knuiman, Bart's avatar
Knuiman, Bart committed
                        if (feature.GeometryType != Tile.GeomType.Polygon)
                            continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                        var polygons = polygonLayers[l][f];
                        if (polygons.vertices.Count == 0)
                            continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                        if (feature.SelectedLayerIdx == 254)
                            continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                        if (feature.RelativeHeight != 0)
                            continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                        var poly = polygonLayers[l][f];
                        for (int t = 0;t < poly.vertices.Count/3;t++)
                        {
                            root.triangles.Add( (l, f, t) );
                        }
Knuiman, Bart's avatar
Knuiman, Bart committed
                stack.Add( root );
                while (stack.Count > 0)
                {
                    var node = stack[0];
                    stack.RemoveAt( 0 );
                    if (node.triangles.Count < 4)
                        continue;
                    if (node.depth > 7)
                        continue;
                    node.children = new QuadTreeNode[4];
                    var hs = (node.max - node.min) / 2;
                    Vector2 [] mins = new [] {
                    node.min,
                    new Vector2(node.min.x+hs.x, node.min.y),
                    new Vector2(node.min.x,      node.min.y+hs.y),
                    new Vector2(node.min.x+hs.x, node.min.y+hs.y)
                };
Knuiman, Bart's avatar
Knuiman, Bart committed
                    for (int i = 0;i < 4;i++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                        var n2   = new QuadTreeNode();
                        n2.depth = node.depth+1;
                        n2.min   = mins[i];
                        n2.max   = n2.min + hs;
                        n2.triangles = new List<(int, int, int)>();
                        for (int t = 0;t < node.triangles.Count;t++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                            int l  = node.triangles[t].Item1;
                            int f  = node.triangles[t].Item2;
                            int t2 = node.triangles[t].Item3;
                            var min2 = polygonLayers[l][f].mins[t2];
                            var max2 = polygonLayers[l][f].maxs[t2];
                            if ((min2.x > n2.max.x) || (min2.y > n2.max.y) || (max2.x < n2.min.x) || (max2.y < n2.min.y))
                            {
                                continue;
                            }
                            n2.triangles.Add( node.triangles[t] );
                            UnityEngine.Debug.Assert( layers[l].VectorTileFeatures[f].SelectedLayerIdx != 254 &&
                                                      layers[l].VectorTileFeatures[f].RelativeHeight == 0 );
Knuiman, Bart's avatar
Knuiman, Bart committed
                        node.children[i] = n2;
                        stack.Add( n2 );
Knuiman, Bart's avatar
Knuiman, Bart committed
                    node.triangles = null;
                }
                UnityEngine.Debug.Log( "Optimize for raytracing took " + sw.ElapsedMilliseconds );
            }
Knuiman, Bart's avatar
Knuiman, Bart committed
            {
                UnityEngine.Debug.LogException( e );
Knuiman, Bart's avatar
Knuiman, Bart committed
        }

Knuiman, Bart's avatar
Knuiman, Bart committed
        // Identify feature by specifying a (unique) index based on some criteria. This can very per
        // vector tile provider. 
        public void IdentifyLayers( Func<VectorTileLayer, List<KeyValuePair<string, object>>, int> selectionCallback,
                                    Func<VectorTileLayer, List<KeyValuePair<string, object>>, int> relativeHeightCallback )
Knuiman, Bart's avatar
Knuiman, Bart committed
            for (int l = 0;l < layers.Count;l++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                var layer  = layers[l];
Knuiman, Bart's avatar
Knuiman, Bart committed
                for (int f = 0;f < layer.VectorTileFeatures.Count;f++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    var feature = layer.VectorTileFeatures[f];
                    feature.SelectedLayerIdx = 254;

Knuiman, Bart's avatar
Knuiman, Bart committed
                    int uniqueId = selectionCallback( layer, feature.Attributes );
                    if (uniqueId > -1)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        feature.SelectedLayerIdx = uniqueId;
Knuiman, Bart's avatar
Knuiman, Bart committed
                    }
                    uniqueId = relativeHeightCallback( layer, feature.Attributes );
                    feature.RelativeHeight = uniqueId;
Knuiman, Bart's avatar
Knuiman, Bart committed
                }
            }
Knuiman, Bart's avatar
Knuiman, Bart committed
            layersIdentified = true;
        }
Knuiman, Bart's avatar
Knuiman, Bart committed

        // Generates a raster for each position (pixel). 2 pixels are pushed in 1 (each 16 bit).
        // If no polygon was hit, 15 is encoded (reserved).
        // If polygon was hit, but no feature was matched, also 15 is encoded.
Knuiman, Bart's avatar
Knuiman, Bart committed
        // Returns a list of failed to match pixels. This can be due to geometry not exactly matching or a layer not being found.
Knuiman, Bart's avatar
Knuiman, Bart committed
        // Also outputs a remap of layer indices. For instance for a specific tile, only layerIdx 1, 6 and 3 are used.
        // Then it applies a remapping of 1->0, 6->1, 3->2. So that in the shader only 3 textures are needed starting from 0, 1.. etc.
Knuiman, Bart's avatar
Knuiman, Bart committed
        public byte[] RenderToTextureSingle(
            int resolution,
            out Dictionary<byte, byte> remappedLayerIndices,
Knuiman, Bart's avatar
Knuiman, Bart committed
            out List<Vector3Int> failedPixels,
Knuiman, Bart's avatar
Knuiman, Bart committed
        {
            Stopwatch sw = new Stopwatch();
            sw.Restart();

            UnityEngine.Debug.Assert( triangulated, "First call Triangulate." );
            UnityEngine.Debug.Assert( layersIdentified, "Identify layers first." );
            UnityEngine.Debug.Assert( resolution > 1, "Must be at least 2." );
Knuiman, Bart's avatar
Knuiman, Bart committed

            byte [] texture = new byte[resolution*resolution/2];
Knuiman, Bart's avatar
Knuiman, Bart committed
            failedPixels = new List<Vector3Int>();
            remappedLayerIndices = new Dictionary<byte, byte>();
Knuiman, Bart's avatar
Knuiman, Bart committed
            float oneOverRes = 1.0f / resolution;
            int cachedTriIdx = 0;
            float fx = 4096 * oneOverRes; // TODO 4096 may be different in other implementations.
Knuiman, Bart's avatar
Knuiman, Bart committed

            QuadTreeNode node = null;
            List<QuadTreeNode> stack = new List<QuadTreeNode>();
Knuiman, Bart's avatar
Knuiman, Bart committed

Knuiman, Bart's avatar
Knuiman, Bart committed
            // For each pixel.
            for (int y = 0;y < resolution && !cancelToken;y++)
Knuiman, Bart's avatar
Knuiman, Bart committed
            {
                for (int x = 0;x < resolution && !cancelToken;x++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                {
Knuiman, Bart's avatar
Knuiman, Bart committed
                    bool hit  = false;
                    Vector2 p = new Vector2(fx*x+0.5f, fx*y+0.5f);
Knuiman, Bart's avatar
Knuiman, Bart committed

                    stack.Add( root );
                    while (stack.Count > 0)
                        node = stack[0];
                        stack.RemoveAt( 0 );
                        if (p.x < node.min.x || p.y < node.min.y || p.x > node.max.x || p.y > node.max.y)
                            for (int c = 0;c < node.children.Length;c++)
                            {
                                stack.Add( node.children[c] );
                            }
                        }
                        else if (node.triangles != null) // Is leaf
                        {
                            for (int t = 0;t < node.triangles.Count;t++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                                int ti = (cachedTriIdx + t)  % node.triangles.Count; // Continue searching where we left off 
                                int l  = node.triangles[ti].Item1;
                                int f  = node.triangles[ti].Item2;
                                int t2 = node.triangles[ti].Item3;
                                var min2 = polygonLayers[l][f].mins[t2];
                                var max2 = polygonLayers[l][f].maxs[t2];
                                if (p.x < min2.x || p.x > max2.x) continue;
                                if (p.y < min2.y || p.y > max2.y) continue;
                                var vertices = polygonLayers[l][f].vertices;
                                var denoms   = polygonLayers[l][f].denoms;
                                hit = GeomUtil.PointIsInsideTriangle2( p, vertices[t2*3], vertices[t2*3+1], vertices[t2*3+2], denoms[t2] );
Knuiman, Bart's avatar
Knuiman, Bart committed
                                if (hit)
                                    var layerIdx = (byte)layers[l].VectorTileFeatures[f].SelectedLayerIdx;
                                    //var addr2    = y*resolution+x;
                                    var addr2    = (resolution - y -1)*resolution+x;
                                    if (layerIdx > 15) layerIdx = 15; // max layers 15 (15 is reserved, nothing was hit). 
                                    var shift = (addr2&1) << 2;
                                    texture[addr2>>1] |= (byte)(layerIdx << shift);
Knuiman, Bart's avatar
Knuiman, Bart committed
                        }

Knuiman, Bart's avatar
Knuiman, Bart committed
                        if (hit) break;
Knuiman, Bart's avatar
Knuiman, Bart committed

Knuiman, Bart's avatar
Knuiman, Bart committed
                    if (!hit)
                    {
                        var addr2 = (resolution - y -1)*resolution+x;
                        var shift = (addr2&1) << 2;
                        texture[addr2>>1] |= (byte)(15 << shift);
                        failedPixels.Add( new Vector3Int( x, y, 15 ) );
                    else
                    {
                        stack.Add( node ); // Add the node that was hit as the next pixel may likely hit this poly again.
                    }
Knuiman, Bart's avatar
Knuiman, Bart committed

Knuiman, Bart's avatar
Knuiman, Bart committed
            // Determine number of different layers. For instance, if only layer 3, 8, 14 and 15 are used, we select
            // a material that only uses 4 textures and put 3 -> Albedo_0 -> 8 to Albedo_1, etc. in the shader.
            int  remappedIndexCounter = -1;
            byte remappedPixel = 15;
            uint addr  = 0;
            byte pixel = 0;
            for (int y = 0;y < resolution && !cancelToken;y++)
Knuiman, Bart's avatar
Knuiman, Bart committed
            {
                for (int x = 0;x < resolution && !cancelToken;x++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                {
                    if ((x&1) == 0)
                        pixel = texture[addr>>1];
                    byte shift  = (byte)((addr&1));
                    byte pixel2 = (byte)((pixel >> (shift<<2)) & 15);
                    if (pixel2 != 15)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        {
                            if (!remappedLayerIndices.TryGetValue( pixel2, out remappedPixel ))
Knuiman, Bart's avatar
Knuiman, Bart committed
                            {
                                if (remappedIndexCounter < 13) // 15 is reserved for not found, so 0-14 are valid.
                                {
                                    remappedIndexCounter++;
                                    remappedPixel = (byte)remappedIndexCounter;
                                    remappedLayerIndices.Add( pixel2, remappedPixel );
                                else remappedPixel = 15; // Reserved for invalid.
Knuiman, Bart's avatar
Knuiman, Bart committed
                            }
Knuiman, Bart's avatar
Knuiman, Bart committed
                        }
                        UnityEngine.Debug.Assert( remappedPixel < 15 );
                    }
                    else
                    {
                        remappedPixel = 15;
                        cachedPixel = 15;
                    }
                    // Data in texture is not zero anymore, so must check if is even, then just set, otherwise OR-add it.
                    if (shift==0)
                        texture[addr>>1] = remappedPixel;
                    else
                        texture[addr>>1] |= (byte)(remappedPixel << 4);
Knuiman, Bart's avatar
Knuiman, Bart committed
                    addr++;
                }
            }

            UnityEngine.Debug.Assert( remappedIndexCounter <= 15, "Exceeded layer count." ); // Invalid pixel is not added to map, so max must be 15, not 16.

            UnityEngine.Debug.Log( "Render to texture took " + sw.ElapsedMilliseconds );
Knuiman, Bart's avatar
Knuiman, Bart committed
            return texture;
Knuiman, Bart's avatar
Knuiman, Bart committed
    {
        public static VectorTile LoadFromUrl( string url, bool autoStart = true )
Knuiman, Bart's avatar
Knuiman, Bart committed
        {
            VectorTile tile = new VectorTile();
Knuiman, Bart's avatar
Knuiman, Bart committed
            tile.request = UnityWebRequest.Get( url );
            if (autoStart)
            {
                tile.StartDownload();
            }
Knuiman, Bart's avatar
Knuiman, Bart committed
            return tile;
        }
Knuiman, Bart's avatar
Knuiman, Bart committed
}