Skip to content

Commit

Permalink
LOD support for tile coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
mpulkki-mapbox committed Nov 13, 2019
1 parent d5248e1 commit c1813da
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 209 deletions.
95 changes: 82 additions & 13 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAlt
import Point from '@mapbox/point-geometry';
import {wrap, clamp} from '../util/util';
import {number as interpolate} from '../style-spec/util/interpolate';
import tileCover from '../util/tile_cover';
import {UnwrappedTileID} from '../source/tile_id';
import EXTENT from '../data/extent';
import {vec4, mat4, mat2} from 'gl-matrix';
import {vec4, mat4, mat2, vec2} from 'gl-matrix';
import {Aabb, Frustum} from '../util/primitives.js';

import type {OverscaledTileID, CanonicalTileID} from '../source/tile_id';
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id';

/**
* A single transform, generally used for a single tile to be
Expand All @@ -34,6 +33,7 @@ class Transform {
cameraToCenterDistance: number;
mercatorMatrix: Array<number>;
projMatrix: Float64Array;
invProjMatrix: Float64Array;
alignedProjMatrix: Float64Array;
pixelMatrix: Float64Array;
pixelMatrixInverse: Float64Array;
Expand Down Expand Up @@ -274,17 +274,85 @@ class Transform {
if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;

const minZoom = options.minzoom || 0;
const centerCoord = MercatorCoordinate.fromLngLat(this.center);
const numTiles = Math.pow(2, z);
const centerPoint = new Point(numTiles * centerCoord.x - 0.5, numTiles * centerCoord.y - 0.5);
const cornerCoords = [
this.pointCoordinate(new Point(0, 0)),
this.pointCoordinate(new Point(this.width, 0)),
this.pointCoordinate(new Point(this.width, this.height)),
this.pointCoordinate(new Point(0, this.height))
];
return tileCover(z, cornerCoords, options.reparseOverscaled ? actualZ : z, this._renderWorldCopies)
.sort((a, b) => centerPoint.dist(a.canonical) - centerPoint.dist(b.canonical));
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z);

// There should always be a certain number of maximum zoom level tiles surrounding the center location
const radiusOfMaxLvlLodInTiles = 3;

const newRootTile = (wrap: number): any => {
return {
// All tiles are on zero elevation plane => z difference is zero
aabb: new Aabb([wrap * numTiles, 0, 0], [(wrap + 1) * numTiles, numTiles, 0]),
zoom: 0,
x: 0,
y: 0,
wrap,
fullyVisible: false
};
};

// Do a depth-first traversal to find visible tiles and proper levels of detail
const stack = [];
const result = [];
const maxZoom = z;
const zOffset = (options.reparseOverscaled ? actualZ : z) - z;

if (this._renderWorldCopies) {
// Render copy of the globe thrice on both sides
for (let i = 1; i <= 3; i++) {
stack.push(newRootTile(-i));
stack.push(newRootTile(i));
}
}

stack.push(newRootTile(0));

// Stream position will determine the "center of the streaming",
// ie. where the most detailed tiles are loaded.
const streamPos = [centerPoint.x, centerPoint.y, 0];

while (stack.length > 0) {
const it = stack.pop();
const x = it.x;
const y = it.y;
let fullyVisible = it.fullyVisible;

// Visibility of a tile is not required if any of its ancestor if fully inside the frustum
if (!fullyVisible) {
const intersectResult = it.aabb.intersects(cameraFrustum);

if (intersectResult === 'none')
continue;

fullyVisible = intersectResult === 'contains';
}

const distanceXY = it.aabb.distanceXY(streamPos);
const longestDim = Math.max(Math.abs(distanceXY[0]), Math.abs(distanceXY[1]));
const distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - it.zoom)) - 2;

// Have we reached the target depth or is the tile too far away to be any split further?
if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) {
result.push({
tileID: new OverscaledTileID(it.zoom + zOffset, it.wrap, it.zoom, x, y),
distanceSq: vec2.dot(distanceXY, distanceXY)
});
continue;
}

for (let i = 0; i < 4; i++) {
const childX = (x << 1) + (i % 2);
const childY = (y << 1) + Math.floor(i / 2);

stack.push({aabb: it.aabb.quadrant(i), zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible});
}
}

return result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID);
}

resize(width: number, height: number) {
Expand Down Expand Up @@ -547,7 +615,7 @@ class Transform {
// (the distance between[width/2, height/2] and [width/2 + 1, height/2])
const halfFov = this._fov / 2;
const groundAngle = Math.PI / 2 + this._pitch;
const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(Math.PI - groundAngle - halfFov);
const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - halfFov, 0.01, Math.PI - 0.01));
const point = this.point;
const x = point.x, y = point.y;

Expand Down Expand Up @@ -583,6 +651,7 @@ class Transform {
mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize, 1]);

this.projMatrix = m;
this.invProjMatrix = mat4.invert([], this.projMatrix);

// Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles.
// We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional
Expand Down
1 change: 0 additions & 1 deletion src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,6 @@ class SourceCache extends Evented {
roundZoom: this._source.roundZoom,
reparseOverscaled: this._source.reparseOverscaled
});

if (this._source.hasTile) {
idealTileIDs = idealTileIDs.filter((coord) => (this._source.hasTile: any)(coord));
}
Expand Down
148 changes: 148 additions & 0 deletions src/util/primitives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// @flow

import {vec3, vec4} from 'gl-matrix';
import assert from 'assert';

type IntersectResult = 'none' | 'intersects' | 'contains';

class Frustum {
points: Array<Array<number>>;
planes: Array<Array<number>>;

constructor(points_: Array<Array<number>>, planes_: Array<Array<number>>) {
this.points = points_;
this.planes = planes_;
}

static fromInvProjectionMatrix(invProj: Float64Array, worldSize: number, zoom: number): Frustum {
const clipSpaceCorners = [
[-1, 1, -1, 1],
[ 1, 1, -1, 1],
[ 1, -1, -1, 1],
[-1, -1, -1, 1],
[-1, 1, 1, 1],
[ 1, 1, 1, 1],
[ 1, -1, 1, 1],
[-1, -1, 1, 1]
];

const scale = Math.pow(2, zoom);

// Transform frustum corner points from clip space to tile space
const frustumCoords = clipSpaceCorners
.map(v => vec4.transformMat4([], v, invProj))
.map(v => vec4.scale([], v, 1.0 / v[3] / worldSize * scale));

const frustumPlanePointIndices = [
[0, 1, 2], // near
[6, 5, 4], // far
[0, 3, 7], // left
[2, 1, 5], // right
[3, 2, 6], // bottom
[0, 4, 5] // top
];

const frustumPlanes = frustumPlanePointIndices.map((p: Array<number>) => {
const a = vec3.sub([], frustumCoords[p[0]], frustumCoords[p[1]]);
const b = vec3.sub([], frustumCoords[p[2]], frustumCoords[p[1]]);
const n = vec3.normalize([], vec3.cross([], a, b));
const d = -vec3.dot(n, frustumCoords[p[1]]);
return n.concat(d);
});

return new Frustum(frustumCoords, frustumPlanes);
}
}

class Aabb {
min: vec3;
max: vec3;
center: vec3;

constructor(min_: vec3, max_: vec3) {
this.min = min_;
this.max = max_;
this.center = vec3.scale([], vec3.add([], this.min, this.max), 0.5);
}

quadrant(index: number): Aabb {
const split = [(index % 2) === 0, index < 2];
const qMin = vec3.clone(this.min);
const qMax = vec3.clone(this.max);
for (let axis = 0; axis < split.length; axis++) {
qMin[axis] = split[axis] ? this.min[axis] : this.center[axis];
qMax[axis] = split[axis] ? this.center[axis] : this.max[axis];
}
// Elevation is always constant, hence quadrant.max.z = this.max.z
qMax[2] = this.max[2];
return new Aabb(qMin, qMax);
}

closestPoint(point: Array<number>): Array<number> {
const x = Math.max(Math.min(this.max[0], point[0]), this.min[0]);
const y = Math.max(Math.min(this.max[1], point[1]), this.min[1]);
return [x, y];
}

distanceXY(point: Array<number>): Array<number> {
const aabbPoint = this.closestPoint(point);
const dx = aabbPoint[0] - point[0];
const dy = aabbPoint[1] - point[1];
return [dx, dy];
}

intersects(frustum: Frustum): IntersectResult {
// Execute separating axis test between two convex objects to find intersections
// Each frustum plane together with 3 major axes define the separating axes
// Note: test only 4 points as both min and max points have equal elevation
assert(this.min[2] === 0 && this.max[2] === 0);

const aabbPoints = [
[this.min[0], this.min[1], 0.0, 1],
[this.max[0], this.min[1], 0.0, 1],
[this.max[0], this.max[1], 0.0, 1],
[this.min[0], this.max[1], 0.0, 1]
];

let fullyInside = true;

for (let p = 0; p < frustum.planes.length; p++) {
const plane = frustum.planes[p];
let pointsInside = 0;

for (let i = 0; i < aabbPoints.length; i++) {
pointsInside += vec4.dot(plane, aabbPoints[i]) >= 0;
}

if (pointsInside === 0)
return 'none';

if (pointsInside !== aabbPoints.length)
fullyInside = false;
}

if (fullyInside)
return 'contains';

for (let axis = 0; axis < 3; axis++) {
let projMin = Number.MAX_VALUE;
let projMax = -Number.MAX_VALUE;

for (let p = 0; p < frustum.points.length; p++) {
const projectedPoint = frustum.points[p][axis] - this.min[axis];

projMin = Math.min(projMin, projectedPoint);
projMax = Math.max(projMax, projectedPoint);
}

if (projMax < 0 || projMin > this.max[axis] - this.min[axis])
return 'none';
}

return 'intersects';
}
}
export {
Aabb,
Frustum
};
100 changes: 0 additions & 100 deletions src/util/tile_cover.js

This file was deleted.

Loading

0 comments on commit c1813da

Please sign in to comment.