Skip to content

Commit

Permalink
use frustum culling in 2d TileLayer
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed May 15, 2020
1 parent 4bedbff commit 3d2a67e
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 67 deletions.
29 changes: 20 additions & 9 deletions examples/website/map-tile/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {render} from 'react-dom';
import DeckGL from '@deck.gl/react';
import {MapView} from '@deck.gl/core';
import {TileLayer} from '@deck.gl/geo-layers';
import {BitmapLayer} from '@deck.gl/layers';
import {BitmapLayer, PathLayer} from '@deck.gl/layers';

const INITIAL_VIEW_STATE = {
latitude: 47.65,
longitude: 7,
zoom: 4.5,
maxZoom: 20,
maxPitch: 89,
bearing: 0
};

Expand Down Expand Up @@ -40,7 +41,7 @@ export default class App extends PureComponent {
}

_renderLayers() {
const {autoHighlight = true, highlightColor = [60, 60, 60, 40]} = this.props;
const {showBorder = false} = this.props;

return [
new TileLayer({
Expand All @@ -49,8 +50,8 @@ export default class App extends PureComponent {

pickable: true,
onHover: this._onHover,
autoHighlight,
highlightColor,
autoHighlight: showBorder,
highlightColor: [60, 60, 60, 40],
// https://wiki.openstreetmap.org/wiki/Zoom_levels
minZoom: 0,
maxZoom: 19,
Expand All @@ -60,11 +61,21 @@ export default class App extends PureComponent {
bbox: {west, south, east, north}
} = props.tile;

return new BitmapLayer(props, {
data: null,
image: props.data,
bounds: [west, south, east, north]
});
return [
new BitmapLayer(props, {
data: null,
image: props.data,
bounds: [west, south, east, north]
}),
showBorder &&
new PathLayer({
id: `${props.id}-border`,
data: [[[west, north], [west, south], [east, south], [east, north], [west, north]]],
getPath: d => d,
getColor: [255, 0, 0],
widthMinPixels: 4
})
];
}
})
];
Expand Down
132 changes: 132 additions & 0 deletions modules/geo-layers/src/tile-layer/tile-2d-traversal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {CullingVolume, Plane, AxisAlignedBoundingBox} from '@math.gl/culling';

const TILE_SIZE = 512;
// number of world copies to check
const MAX_MAPS = 3;

class OSMNode {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}

get children() {
if (!this._children) {
const x = this.x * 2;
const y = this.y * 2;
const z = this.z + 1;
this._children = [
new OSMNode(x, y, z),
new OSMNode(x, y + 1, z),
new OSMNode(x + 1, y, z),
new OSMNode(x + 1, y + 1, z)
];
}
return this._children;
}

update(params) {
const {viewport, cullingVolume, elevationBounds, minZ, maxZ, offset} = params;
const boundingVolume = this.getBoundingVolume(elevationBounds, offset);

// First, check if this tile is visible
const isInside = cullingVolume.computeVisibility(boundingVolume);
if (isInside < 0) {
return false;
}

// Avoid loading overlapping tiles - if a descendant is requested, do not request the ancester
if (!this.childVisible) {
let {z} = this;
if (z < maxZ && z >= minZ) {
// Adjust LOD by distance to camera
const distance =
(boundingVolume.distanceTo(viewport.cameraPosition) * viewport.scale) / viewport.height;
z += Math.floor(Math.log2(distance));
}
if (z >= maxZ) {
// LOD is acceptable
this.selected = true;
return true;
}
}

// LOD is not enough, recursively test child tiles
this.selected = false;
this.childVisible = true;
for (const child of this.children) {
child.update(params);
}
return true;
}

getSelected(result = []) {
if (this.selected) {
result.push(this);
}
if (this._children) {
for (const node of this._children) {
node.getSelected(result);
}
}
return result;
}

getBoundingVolume(zRange, worldOffset) {
const scale = Math.pow(2, this.z);
const extent = TILE_SIZE / scale;
const originX = this.x * extent + worldOffset * TILE_SIZE;
// deck's common space is y-flipped
const originY = TILE_SIZE - (this.y + 1) * extent;

return new AxisAlignedBoundingBox(
[originX, originY, zRange[0]],
[originX + extent, originY + extent, zRange[1]]
);
}
}

export function getOSMTileIndices(viewport, maxZ, zRange) {
// Get the culling volume of the current camera
const planes = Object.values(viewport.getFrustumPlanes()).map(
({normal, distance}) => new Plane(normal.clone().negate(), distance)
);
const cullingVolume = new CullingVolume(planes);

// Project zRange from meters to common space
const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2];
const elevationMin = (zRange && zRange[0] * unitsPerMeter) || 0;
const elevationMax = (zRange && zRange[1] * unitsPerMeter) || 0;

// Always load at the current zoom level if pitch is small
const minZ = viewport.pitch <= 60 ? maxZ : 0;

const root = new OSMNode(0, 0, 0);
const traversalParams = {
viewport,
cullingVolume,
elevationBounds: [elevationMin, elevationMax],
minZ,
maxZ,
offset: 0
};

root.update(traversalParams);

// Check worlds in repeated maps
traversalParams.offset = -1;
while (root.update(traversalParams)) {
if (--traversalParams.offset < -MAX_MAPS) {
break;
}
}
traversalParams.offset = 1;
while (root.update(traversalParams)) {
if (++traversalParams.offset > -MAX_MAPS) {
break;
}
}

return root.getSelected();
}
2 changes: 1 addition & 1 deletion modules/geo-layers/src/tile-layer/tileset-2d.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default class Tileset2D {
* @param {*} onUpdate
*/
update(viewport, {zRange} = {}) {
if (viewport !== this._viewport) {
if (!viewport.equals(this._viewport)) {
this._viewport = viewport;
const tileIndices = this.getTileIndices({
viewport,
Expand Down
46 changes: 1 addition & 45 deletions modules/geo-layers/src/tile-layer/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {lngLatToWorld} from '@math.gl/web-mercator';
import {getOSMTileIndices} from './tile-2d-traversal';

const TILE_SIZE = 512;
const DEFAULT_EXTENT = [-Infinity, -Infinity, Infinity, Infinity];
Expand Down Expand Up @@ -66,17 +66,6 @@ function getBoundingBox(viewport, zRange, extent) {
];
}

/*
* get the OSM tile index at the given location
* https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
*/
function getOSMTileIndex(lngLat, scale) {
let [x, y] = lngLatToWorld(lngLat);
x *= scale / TILE_SIZE;
y = (1 - y / TILE_SIZE) * scale;
return [x, y];
}

function getTileIndex([x, y], scale) {
return [(x * scale) / TILE_SIZE, (y * scale) / TILE_SIZE];
}
Expand Down Expand Up @@ -130,39 +119,6 @@ function getIdentityTileIndices(viewport, z, tileSize, extent) {
return indices;
}

function getOSMTileIndices(viewport, z, zRange, extent) {
const bbox = getBoundingBox(viewport, zRange, extent);
const scale = getScale(z);
/*
minX, maxX could be out of bounds if longitude is near the 180 meridian or multiple worlds
are shown:
| |
actual -2 -1 0 1 2 3
expected 2 3 0 1 2 3
*/
let [minX, minY] = getOSMTileIndex([bbox[0], bbox[3]], scale);
let [maxX, maxY] = getOSMTileIndex([bbox[2], bbox[1]], scale);
const indices = [];

/*
| TILE | TILE | TILE |
|(minX) |(maxX)
*/
minX = Math.floor(minX);
maxX = Math.min(minX + scale, maxX); // Avoid creating duplicates
minY = Math.max(0, Math.floor(minY));
maxY = Math.min(scale, maxY);
for (let x = minX; x < maxX; x++) {
for (let y = minY; y < maxY; y++) {
// Cast to valid x between [0, scale]
const normalizedX = x - Math.floor(x / scale) * scale;
indices.push({x: normalizedX, y, z});
}
}

return indices;
}

/**
* Returns all tile indices in the current viewport. If the current zoom level is smaller
* than minZoom, return an empty array. If the current zoom level is greater than maxZoom,
Expand Down
40 changes: 30 additions & 10 deletions test/modules/geo-layers/tile-layer/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,53 @@ const TEST_CASES = [
minZoom: undefined,
maxZoom: undefined,
output: [
'0,1,3',
'0,2,3',
'0,3,3',
'1,1,3',
'1,2,3',
'1,3,3',
'2,1,3',
'2,2,3',
'2,3,3',
'3,1,3',
'3,2,3',
'3,3,3'
'3,3,3',
'7,2,3'
]
},
{
title: 'flat viewport (exact)',
title: 'extreme pitch',
viewport: new WebMercatorViewport({
width: 1024,
height: 1024,
width: 800,
height: 400,
pitch: 75,
bearing: 0,
longitude: 0,
latitude: 0,
orthographic: true,
zoom: 2
zoom: 4
}),
minZoom: undefined,
maxZoom: undefined,
output: ['1,1,2', '1,2,2', '2,1,2', '2,2,2']
output: [
'0,0,2',
'1,0,2',
'2,0,2',
'2,2,3',
'2,3,3',
'3,0,2',
'3,2,3',
'4,2,3',
'5,2,3',
'5,3,3',
'6,6,4',
'6,7,4',
'7,6,4',
'7,7,4',
'7,8,4',
'8,6,4',
'8,7,4',
'8,8,4',
'9,6,4',
'9,7,4'
]
},
{
title: 'under zoom',
Expand Down
4 changes: 2 additions & 2 deletions website/src/components/demos/map-tile-demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import App from 'website-examples/map-tile/app';
export default class MapTileDemo extends Component {
static get parameters() {
return {
autoHighlight: {displayName: 'Highlight on hover', type: 'checkbox', value: true}
showBorder: {displayName: 'Show tile borders', type: 'checkbox', value: false}
};
}

Expand All @@ -29,6 +29,6 @@ export default class MapTileDemo extends Component {
render() {
// eslint-disable-next-line no-unused-vars
const {params, data, ...otherProps} = this.props;
return <App {...otherProps} autoHighlight={params.autoHighlight.value} />;
return <App {...otherProps} showBorder={params.showBorder.value} />;
}
}

0 comments on commit 3d2a67e

Please sign in to comment.