Newer
Older
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
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.
public List<Vector2> vertices;
public List<Vector2> mins;
public List<Vector2> maxs;
public List<float> denoms;
public void OptimizeForIsPointInTriangle()
{
if ( vertices == null ) return; // Non poly layers have this.
mins = new List<Vector2>( vertices.Count / 3 );
maxs = new List<Vector2>( vertices.Count / 3 );
denoms = new List<float>( vertices.Count / 3 );
for ( int i = 0; i < vertices.Count; i += 3 )
{
float minx = float.MaxValue;
float miny = float.MaxValue;
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 );
public bool Valid => valid;
public bool Finished => finished;
public bool Triangulated => triangulated;
private bool valid;
private bool finished;
private bool triangulated;
private Task parseTileTask;
private List<VectorTileLayer> layers;
private List<List<TriangulatedPolygon>> polygonLayers;
private QuadTreeNode root;
internal UnityWebRequest request;
public void StartDownload()
{
UnityEngine.Debug.Assert(!started);
public bool IsFinished()
{
if (finished)
return true;
if (!request.isDone)
return false;
layers = VectorTileParser.Parse( stream );
} );
}
else if (parseTileTask.IsCompleted)
{
valid = parseTileTask.IsCompletedSuccessfully;
finished = true;
parseTileTask = null;
}
}
return finished;
}
static bool IsTriangleCCW( Vector2 vertex1, Vector2 vertex2, Vector2 vertex3 )
{
float signedArea = (vertex2.x - vertex1.x) * (vertex3.y - vertex1.y) - (vertex3.x - vertex1.x) * (vertex2.y - vertex1.y);
return signedArea > 0;
}
// Number of failed polys to triangulate are returned.
public int Triangulate()
if (triangulated )
return 0;
int numFailedPolys = 0;
polygonLayers = new List<List<TriangulatedPolygon>>();
List<double> vertices = new List<double>();
List<int> holeIndices = new List<int>();
for ( int i =0; i < layers.Count; i++ )
{
var features = layers[i].VectorTileFeatures;
var polygons = new List<TriangulatedPolygon>();
for (int j = 0; j< features.Count; j++)
{
polygons.Add( new TriangulatedPolygon() ); // Add empty to ensure list(layer) of lists(features) match.
if (features[j].GeometryType != Tile.GeomType.Polygon)
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 ) );
}
//for ( int k = 0; k < indices.Count; k += 3)
//{
// if ( !IsTriangleCCW( poly.vertices[k], poly.vertices[k+1], poly.vertices[k+2] ))
// {
// var t = poly.vertices[k];
// poly.vertices[k] = poly.vertices[k+2];
// poly.vertices[k+2] = t;
// }
//}
UnityEngine.Debug.Assert( poly.vertices.Count % 3 == 0 );
}
catch (Exception)
{
numFailedPolys++;
}
}
polygonLayers.Add( polygons );
}
triangulated = true;
return numFailedPolys;
}
Stopwatch sw = new Stopwatch();
sw.Restart();
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
}
}
// 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++)
{
var layer = layers[l];
for (int f = 0;f < layer.VectorTileFeatures.Count;f++)
{
var feature = layer.VectorTileFeatures[f];
if (feature.GeometryType != Tile.GeomType.Polygon)
continue;
var polygons = polygonLayers[l][f];
if (polygons.vertices.Count == 0)
continue;
if (feature.SelectedLayerIdx == 254)
continue;
if (feature.RelativeHeight > 0)
continue;
var poly = polygonLayers[l][f];
for (int t = 0;t < poly.vertices.Count/3;t++)
{
root.triangles.Add( (l, f, t) );
}
}
}
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)
};
for (int i = 0;i < 4;i++)
{
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++ )
{
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( (l, f, t2) );
}
node.children[i] = n2;
stack.Add( n2 );
}
node.triangles = null;
}
UnityEngine.Debug.Log( "Optimize for raytracing took " + sw.ElapsedMilliseconds );
// 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 )
var feature = layer.VectorTileFeatures[f];
feature.SelectedLayerIdx = 254;
int uniqueId = selectionCallback( layer, feature.Attributes );
if (uniqueId > -1)
uniqueId = relativeHeightCallback( layer, feature.Attributes );
feature.RelativeHeight = uniqueId;
// Calls callback(x, y, channel) for each raster position (256 channels) where each value represents a single channel.
// If no polygon was hit, 255 is called.
// If polygon was hit, but no feature was matched, 254 is called.
// Returns a list of failed to match pixels. This can be due to geometry not exactly matching or a layer not being found.
// Also out puts a remap of layer indices. For instance for a specific tile, only layerIdx 1, 6 and 3 are used.
// Then this a remapping of 1->0, 6->1, 3->2. So that in the shader only 3 textures are needed starting from 0, 1.. etc.
public byte[] RenderToTextureSingle(
int resolution,
out Dictionary<byte, byte> remappedLayerIndices,
out List<Vector3Int> failedPixels,
ref bool cancelToken )
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." );
byte [] texture = new byte[resolution*resolution];
failedPixels = new List<Vector3Int>();
remappedLayerIndices = new Dictionary<byte, byte>();
int cachedTriIdx = 0;
float fx = 4096 * oneOverRes; // TODO 4096 may be different in other implementations.
QuadTreeNode node = null;
List<QuadTreeNode> stack = new List<QuadTreeNode>();
for (int x = 0;x < resolution && !cancelToken ;x++)
Vector2 p = new Vector2(fx*x+0.5f, fx*y+0.5f);
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)
continue;
}
if ( node.children != null )
{
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++ )
int ti = (cachedTriIdx + t) % node.triangles.Count;
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] );
// TODO caching the TriIdx doees not work, I have no clue why, it should only function
// as a hint, as where to start. But using this hint results in massive number of triangles going wrong...
// cachedTriIdx = ti;
var layerIdx = (byte)layers[l].VectorTileFeatures[f].SelectedLayerIdx;
texture[(resolution - y -1)*resolution+x] = layerIdx;
break;
}
}
} // end while
stack.Clear();
if (!hit)
{
texture[(resolution - y -1)*resolution+x] = 255;
failedPixels.Add( new Vector3Int( x, y, 255 ) );
else
{
stack.Add( node ); // Add the node that was hit as the next pixel may likely hit this poly again.
}
// 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.
byte cachedPixel = 255;
int remappedIndexCounter = -1;
int addr = 0;
for (int y = 0;y < resolution && !cancelToken;y++)
{
for (int x = 0;x < resolution;x++)
{
byte pixel = texture[addr];
if (pixel != cachedPixel)
if (!remappedLayerIndices.TryGetValue( pixel, out remappedPixel ))
if (remappedIndexCounter < 253) // 254 is reserved for no layer match, 255 is ray hit with triangle.
{
remappedIndexCounter++;
remappedPixel = (byte)remappedIndexCounter;
remappedLayerIndices.Add( pixel, remappedPixel );
}
else remappedPixel = 0;
cachedPixel = pixel;
texture[addr] = remappedPixel;
} // leave unfound or unhit pixels on their original value.
UnityEngine.Debug.Assert(remappedIndexCounter <= 254, "Exceeded layer count." );
UnityEngine.Debug.Log( "Render to texture took " + sw.ElapsedMilliseconds );
public static VectorTile LoadFromUrl( string url, bool autoStart=true )
tile.request = UnityWebRequest.Get( url );
if (autoStart)
{
tile.StartDownload();
}