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

Fix level of detail at high pitch #4741

Closed
wants to merge 6 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Fix circle won't render on mesa 24.1 with AMD GPU ([#4062](https://github.com/maplibre/maplibre-gl-js/issues/4062))
- Fix hash router for urls ending with a hashtag ([#4730](https://github.com/maplibre/maplibre-gl-js/pull/4730))
- Replace rollup-plugin-sourcemaps with rollup-plugin-sourcemaps2 ([#4740](https://github.com/maplibre/maplibre-gl-js/pull/4740))
- Fix level of detail at high pitch angle ([#3983](https://github.com/maplibre/maplibre-gl-js/issues/3983))
- _...Add new stuff here..._

## 4.7.0
Expand Down
36 changes: 35 additions & 1 deletion src/geo/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('transform', () => {
tileSize: 512
};

const transform = new Transform(0, 22, 0, 60, true);
const transform = new Transform(0, 22, 0, 85, true);
transform.resize(200, 200);

test('general', () => {
Expand Down Expand Up @@ -248,6 +248,24 @@ describe('transform', () => {
new OverscaledTileID(5, 0, 5, 22, 7)
]);

transform.zoom = 8;
transform.pitch = 85.0;
transform.bearing = 0.0;
transform.center = new LngLat(20.918, 39.232);
transform.resize(50, 1000);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(8, 0, 8, 142, 97),
new OverscaledTileID(8, 0, 8, 142, 98),
new OverscaledTileID(8, 0, 8, 142, 96),
new OverscaledTileID(7, 0, 7, 71, 47),
new OverscaledTileID(7, 0, 7, 71, 46),
new OverscaledTileID(6, 0, 6, 35, 22),
new OverscaledTileID(5, 0, 5, 17, 10),
new OverscaledTileID(9, 0, 9, 285, 198),
new OverscaledTileID(10, 0, 10, 571, 398),
new OverscaledTileID(10, 0, 10, 571, 399)
]);

transform.zoom = 8;
transform.pitch = 60;
transform.bearing = 45.0;
Expand Down Expand Up @@ -315,6 +333,22 @@ describe('transform', () => {
});

});

test('maxzoom-0', () => {
const options = {
minzoom: 0,
maxzoom: 0,
tileSize: 512
};

const transform = new Transform(0, 0, 0, 60, true);
transform.resize(200, 200);
transform.center = new LngLat(0.01, 0.01);
transform.zoom = 8;
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(0, 0, 0, 0, 0)
]);
});

test('coveringZoomLevel', () => {
const options = {
Expand Down
63 changes: 31 additions & 32 deletions src/geo/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,27 +353,22 @@ export class Transform {
terrain?: Terrain;
}
): Array<OverscaledTileID> {
let z = this.coveringZoomLevel(options);
const actualZ = z;
let nominalZ = this.coveringZoomLevel(options);

if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;
const minZoom = options.minzoom || 0;
const maxZoom = options.maxzoom !== undefined ? options.maxzoom : this.maxZoom;
nominalZ = Math.min(Math.max(0, nominalZ), maxZoom);
let actualZ = nominalZ;

const cameraCoord = this.pointCoordinate(this.getCameraPoint());
const centerCoord = MercatorCoordinate.fromLngLat(this.center);
const numTiles = Math.pow(2, z);
const numTiles = Math.pow(2, nominalZ);
const cameraPoint = [numTiles * cameraCoord.x, numTiles * cameraCoord.y, 0];
const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invModelViewProjectionMatrix, this.worldSize, z);

// No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level
let minZoom = options.minzoom || 0;
// Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks
if (!options.terrain && this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
minZoom = z;

// There should always be a certain number of maximum zoom level tiles surrounding the center location in 2D or in front of the camera in 3D
const radiusOfMaxLvlLodInTiles = options.terrain ? 2 / Math.min(this.tileSize, options.tileSize) * this.tileSize : 3;
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invModelViewProjectionMatrix, this.worldSize, nominalZ);
const distanceToCenter2d = Math.hypot(centerPoint[0] - cameraPoint[0], centerPoint[1] - cameraPoint[1]);
const distanceZ = distanceToCenter2d / Math.max(0.001, Math.tan(this._pitch));
const distanceToCenter3d = Math.hypot(distanceToCenter2d, distanceZ);

const newRootTile = (wrap: number): any => {
return {
Expand All @@ -389,8 +384,6 @@ export class Transform {
// 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
Expand Down Expand Up @@ -418,21 +411,27 @@ export class Transform {
fullyVisible = intersectResult === 2;
}

const refPoint = options.terrain ? cameraPoint : centerPoint;
const distanceX = it.aabb.distanceX(refPoint);
const distanceY = it.aabb.distanceY(refPoint);
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;

// 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)) {
const dz = maxZoom - it.zoom, dx = cameraPoint[0] - 0.5 - (x << dz), dy = cameraPoint[1] - 0.5 - (y << dz);
const distanceX = it.aabb.distanceX(cameraPoint);
const distanceY = it.aabb.distanceY(cameraPoint);
const distToTile2d = Math.hypot(distanceX, distanceY);
const distToTile3d = Math.hypot(distanceZ, distToTile2d);

// No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level
// Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks
if (options.terrain || this.pitch > 60.0 || this._edgeInsets.top >= 0.1) {
actualZ = (options.roundZoom ? Math.round : Math.floor)(
this.zoom + this.scaleZoom(this.tileSize / options.tileSize * distanceToCenter3d / distToTile3d)
);
}
const z = Math.min(actualZ, maxZoom);

// Have we reached the target depth?
if (it.zoom >= z) {
if (it.zoom < minZoom) {
continue;
}
const dz = nominalZ - it.zoom, dx = cameraPoint[0] - 0.5 - (x << dz), dy = cameraPoint[1] - 0.5 - (y << dz);
const overscaledZ = options.reparseOverscaled ? actualZ : it.zoom;
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]),
Expand Down