Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LOD support for tile coverage #8975

Merged
merged 4 commits into from
Jan 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 90 additions & 14 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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: type removal needed because of new UnwrappedTileID and new OverscaledTileID. Although type can stay in front of CanonicalTileID, it is good to put them all in the same line.


/**
* 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 @@ -278,15 +278,90 @@ class Transform {

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 centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z);

// No change of LOD behavior for pitch lower than 60: return only tile ids from the requested zoom level
let minZoom = options.minzoom || 0;
if (this.pitch <= 60.0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: consider joining this with minZoom definition (L279) and with a comment like: // No change of LOD behavior for pitch lower than 60: return only one LOD zoom level.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astojilj @mpulkki-mapbox sorry I'm a bit late here, but any reason for not doing LOD for pitch <= 60? Even at pitch 60 especially when you have a bearing like 45 degrees and using 256x256 satellite tiles it feels like some of those tiles right at furthest point away probably could be LOD.

minZoom = 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 overscaledZ = options.reparseOverscaled ? actualZ : 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));

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 === 0)
continue;

fullyVisible = intersectResult === 2;
}

const distanceX = it.aabb.distanceX(centerPoint);
const distanceY = it.aabb.distanceY(centerPoint);
const longestDim = Math.max(Math.abs(distanceX), Math.abs(distanceY));

// We're using distance based heuristics to determine if a tile should be split into quadrants or not.
// radiusOfMaxLvlLodInTiles defines that there's always a certain number of maxLevel tiles next to the map center.
// Using the fact that a parent node in quadtree is twice the size of its children (per dimension)
// we can define distance thresholds for each relative level:
// f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k. This is the same as "offset+2^(k+1)-2"
const distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - it.zoom)) - 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radiusOfMaxLvlLodInTiles - 2 = 3 - 2 = 1. 😅 this formula needs clarification in comment or diagram in PR: usage of radiusOfMaxLvlLodInTilesa and -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 === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y),
distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y])
});
continue;
}

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

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 @@ -549,7 +624,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));
Copy link
Contributor Author

@mpulkki-mapbox mpulkki-mapbox Nov 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Projection matrix generation was broken with high pitch values. topHalfSurfaceDistance might have gotten negative value causing malformed projection matrix. This could be seen as either disappearing ground or as floating symbols.

const point = this.point;
const x = point.x, y = point.y;

Expand Down Expand Up @@ -585,6 +660,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
145 changes: 145 additions & 0 deletions src/util/primitives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// @flow

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

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);
}

distanceX(point: Array<number>): number {
const pointOnAabb = Math.max(Math.min(this.max[0], point[0]), this.min[0]);
return pointOnAabb - point[0];
}

distanceY(point: Array<number>): number {
const pointOnAabb = Math.max(Math.min(this.max[1], point[1]), this.min[1]);
return pointOnAabb - point[1];
}

// Performs a frustum-aabb intersection test. Returns 0 if there's no intersection,
// 1 if shapes are intersecting and 2 if the aabb if fully inside the frustum.
intersects(frustum: Frustum): number {
// 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 0;

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

if (fullyInside)
return 2;

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 0;
}

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

This file was deleted.

Loading