Skip to content

Commit

Permalink
Globe: symbol and coveringTiles optimizations (#4778)
Browse files Browse the repository at this point in the history
* Import changes from dev branch

* Add tileBelowHorizon unit test

* Revert coveringTiles changes

* Add projectTileCoordinatesToSphere doccomment

* Revert globe coveringTiles changes

* Expose globe transform's _globeRendering

* Remove unneeded float32 matrix

* Update build size

* Remove unused code + rename isGlobeRendering to use proper camelCase

* Remove unnecessary matrix conversions to float32

* Add changelog entry

* Revert failing render test
  • Loading branch information
kubapelc authored Oct 15, 2024
1 parent 3d4d7c6 commit 51bd024
Show file tree
Hide file tree
Showing 18 changed files with 257 additions and 131 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

### 🐞 Bug fixes
- Fix a memory leak due to missing removal of event listener registration ([#4824](https://github.com/maplibre/maplibre-gl-js/pull/4824))
- Improve symbol collision performance for both mercator and globe projections ([#4778](https://github.com/maplibre/maplibre-gl-js/pull/4778))
- Fix bad line scaling near the poles under globe projection ([#4778](https://github.com/maplibre/maplibre-gl-js/pull/4778))
- Fix globe loading many tiles at an unnecessarily high zoom level when the camera is pitched ([#4778](https://github.com/maplibre/maplibre-gl-js/pull/4778))
- _...Add new stuff here..._

## 5.0.0-pre.1
Expand Down
10 changes: 5 additions & 5 deletions src/geo/projection/globe_covering_tiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function shouldSplitTile(centerCoord: MercatorCoordinate, cameraCoord: MercatorC
// This logic might be slightly different from what mercator_transform.ts does, but should result in very similar (if not the same) set of tiles being loaded.
const centerDist = distanceToTile(centerCoord.x, centerCoord.y, tileX, tileY, tileSize);
const cameraDist = distanceToTile(cameraCoord.x, cameraCoord.y, tileX, tileY, tileSize);
return Math.min(centerDist, cameraDist) * 2 <= radiusOfMaxLvlLodInTiles; // Multiply distance by 2, because the subdivided tiles would be half the size
return Math.min(centerDist, cameraDist) * 2 <= radiusOfMaxLvlLodInTiles * tileSize; // Multiply distance by 2, because the subdivided tiles would be half the size
}

// Returns the wrap value for a given tile, computed so that tiles will remain loaded when crossing the antimeridian.
Expand Down Expand Up @@ -153,10 +153,10 @@ export function getTileAABB(tileID: {x: number; y: number; z: number}): Aabb {
// Compute AABB using the 4 corners.

const corners = [
projectTileCoordinatesToSphere(0, 0, tileID),
projectTileCoordinatesToSphere(EXTENT, 0, tileID),
projectTileCoordinatesToSphere(EXTENT, EXTENT, tileID),
projectTileCoordinatesToSphere(0, EXTENT, tileID),
projectTileCoordinatesToSphere(0, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, EXTENT, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(0, EXTENT, tileID.x, tileID.y, tileID.z),
];

const min: vec3 = [1, 1, 1];
Expand Down
8 changes: 4 additions & 4 deletions src/geo/projection/globe_transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ describe('GlobeTransform', () => {
const globeTransform = createGlobeTransform(globeProjectionMock);

expect(globeTransform.getGlobeViewAllowed()).toBe(true);
expect(globeTransform.useGlobeControls).toBe(true);
expect(globeTransform.isGlobeRendering).toBe(true);
});

test('animates to false', async () => {
Expand All @@ -450,12 +450,12 @@ describe('GlobeTransform', () => {
await sleep(20);
globeTransform.newFrameUpdate();
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
expect(globeTransform.useGlobeControls).toBe(true);
expect(globeTransform.isGlobeRendering).toBe(true);

await sleep(1000);
globeTransform.newFrameUpdate();
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
expect(globeTransform.useGlobeControls).toBe(false);
expect(globeTransform.isGlobeRendering).toBe(false);
});

test('can skip animation if requested', async () => {
Expand All @@ -466,7 +466,7 @@ describe('GlobeTransform', () => {
await sleep(20);
globeTransform.newFrameUpdate();
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
expect(globeTransform.useGlobeControls).toBe(false);
expect(globeTransform.isGlobeRendering).toBe(false);
});
});

Expand Down
72 changes: 42 additions & 30 deletions src/geo/projection/globe_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,12 @@ export class GlobeTransform implements ITransform {
private _projectionInstance: GlobeProjection;
private _globeLatitudeErrorCorrectionRadians: number = 0;

private get _globeRendering(): boolean {
/**
* True when globe render path should be used instead of the old but simpler mercator rendering.
* Globe automatically transitions to mercator at high zoom levels, which causes a switch from
* globe to mercator render path.
*/
get isGlobeRendering(): boolean {
return this._globeness > 0;
}

Expand Down Expand Up @@ -295,13 +300,11 @@ export class GlobeTransform implements ITransform {
this._globeLatitudeErrorCorrectionRadians = that._globeLatitudeErrorCorrectionRadians;
}

public get projectionMatrix(): mat4 { return this._globeRendering ? this._projectionMatrix : this._mercatorTransform.projectionMatrix; }

public get modelViewProjectionMatrix(): mat4 { return this._globeRendering ? this._globeViewProjMatrixNoCorrection : this._mercatorTransform.modelViewProjectionMatrix; }
public get projectionMatrix(): mat4 { return this.isGlobeRendering ? this._projectionMatrix : this._mercatorTransform.projectionMatrix; }

public get inverseProjectionMatrix(): mat4 { return this._globeRendering ? this._globeProjMatrixInverted : this._mercatorTransform.inverseProjectionMatrix; }
public get modelViewProjectionMatrix(): mat4 { return this.isGlobeRendering ? this._globeViewProjMatrixNoCorrection : this._mercatorTransform.modelViewProjectionMatrix; }

public get useGlobeControls(): boolean { return this._globeRendering; }
public get inverseProjectionMatrix(): mat4 { return this.isGlobeRendering ? this._globeProjMatrixInverted : this._mercatorTransform.inverseProjectionMatrix; }

public get cameraPosition(): vec3 {
// Return a copy - don't let outside code mutate our precomputed camera position.
Expand Down Expand Up @@ -358,12 +361,12 @@ export class GlobeTransform implements ITransform {
this._updateErrorCorrectionValue();

this._lastUpdateTime = browser.now();
const oldGlobeRendering = this._globeRendering;
const oldGlobeRendering = this.isGlobeRendering;
this._globeness = this._computeGlobenessAnimation();

this._calcMatrices();

if (oldGlobeRendering === this._globeRendering) {
if (oldGlobeRendering === this.isGlobeRendering) {
return {
forcePlacementUpdate: false,
};
Expand All @@ -372,7 +375,7 @@ export class GlobeTransform implements ITransform {
forcePlacementUpdate: true,
fireProjectionEvent: {
type: 'projectiontransition',
newProjection: this._globeRendering ? 'globe' : 'globe-mercator',
newProjection: this.isGlobeRendering ? 'globe' : 'globe-mercator',
},
forceSourceUpdate: true,
};
Expand All @@ -387,7 +390,7 @@ export class GlobeTransform implements ITransform {
if (!this._projectionInstance) {
return;
}
this._projectionInstance.useGlobeRendering = this._globeRendering;
this._projectionInstance.useGlobeRendering = this.isGlobeRendering;
this._projectionInstance.errorQueryLatitudeDegrees = this.center.lat;
this._globeLatitudeErrorCorrectionRadians = this._projectionInstance.latitudeErrorCorrectionRadians;
}
Expand Down Expand Up @@ -447,7 +450,7 @@ export class GlobeTransform implements ITransform {
const data = this._mercatorTransform.getProjectionData(overscaledTileID, aligned, ignoreTerrainMatrix);

// Set 'projectionMatrix' to actual globe transform
if (this._globeRendering) {
if (this.isGlobeRendering) {
data.mainMatrix = this._globeViewProjMatrix;
}

Expand Down Expand Up @@ -561,28 +564,30 @@ export class GlobeTransform implements ITransform {
}

public getPixelScale(): number {
return 1.0 / Math.cos(this.getAnimatedLatitude() * Math.PI / 180);
return lerp(this._mercatorTransform.getPixelScale(), 1.0 / Math.cos(this.getAnimatedLatitude() * Math.PI / 180), this._globeness);
}

public getCircleRadiusCorrection(): number {
return Math.cos(this.getAnimatedLatitude() * Math.PI / 180);
return lerp(this._mercatorTransform.getCircleRadiusCorrection(), Math.cos(this.getAnimatedLatitude() * Math.PI / 180), this._globeness);
}

public getPitchedTextCorrection(textAnchor: Point, tileID: UnwrappedTileID): number {
if (!this._globeRendering) {
return 1.0;
public getPitchedTextCorrection(textAnchorX: number, textAnchorY: number, tileID: UnwrappedTileID): number {
const mercatorCorrection = this._mercatorTransform.getPitchedTextCorrection(textAnchorX, textAnchorY, tileID);
if (!this.isGlobeRendering) {
return mercatorCorrection;
}
const mercator = tileCoordinatesToMercatorCoordinates(textAnchor.x, textAnchor.y, tileID.canonical);
const mercator = tileCoordinatesToMercatorCoordinates(textAnchorX, textAnchorY, tileID.canonical);
const angular = mercatorCoordinatesToAngularCoordinatesRadians(mercator.x, mercator.y);
return this.getCircleRadiusCorrection() / Math.cos(angular[1]);
return lerp(mercatorCorrection, this.getCircleRadiusCorrection() / Math.cos(angular[1]), this._globeness);
}

public projectTileCoordinates(x: number, y: number, unwrappedTileID: UnwrappedTileID, getElevation: (x: number, y: number) => number): PointProjection {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.projectTileCoordinates(x, y, unwrappedTileID, getElevation);
}

const spherePos = projectTileCoordinatesToSphere(x, y, unwrappedTileID.canonical);
const canonical = unwrappedTileID.canonical;
const spherePos = projectTileCoordinatesToSphere(x, y, canonical.x, canonical.y, canonical.z);
const elevation = getElevation ? getElevation(x, y) : 0.0;
const vectorMultiplier = 1.0 + elevation / earthRadius;
const pos: vec4 = [spherePos[0] * vectorMultiplier, spherePos[1] * vectorMultiplier, spherePos[2] * vectorMultiplier, 1];
Expand Down Expand Up @@ -681,7 +686,7 @@ export class GlobeTransform implements ITransform {
}

coveringTiles(options: CoveringTilesOptions): OverscaledTileID[] {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.coveringTiles(options);
}

Expand Down Expand Up @@ -711,7 +716,7 @@ export class GlobeTransform implements ITransform {
}

lngLatToCameraDepth(lngLat: LngLat, elevation: number): number {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.lngLatToCameraDepth(lngLat, elevation);
}
if (!this._globeViewProjMatrixNoCorrection) {
Expand All @@ -729,7 +734,7 @@ export class GlobeTransform implements ITransform {
}

getBounds(): LngLatBounds {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.getBounds();
}

Expand Down Expand Up @@ -823,7 +828,7 @@ export class GlobeTransform implements ITransform {
* (same size before and after a {@link setLocationAtPoint} call).
*/
setLocationAtPoint(lnglat: LngLat, point: Point): void {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
this._mercatorTransform.setLocationAtPoint(lnglat, point);
this.apply(this._mercatorTransform);
return;
Expand Down Expand Up @@ -925,7 +930,7 @@ export class GlobeTransform implements ITransform {
}

locationToScreenPoint(lnglat: LngLat, terrain?: Terrain): Point {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.locationToScreenPoint(lnglat, terrain);
}

Expand Down Expand Up @@ -955,7 +960,7 @@ export class GlobeTransform implements ITransform {
}

screenPointToMercatorCoordinate(p: Point, terrain?: Terrain): MercatorCoordinate {
if (!this._globeRendering || terrain) {
if (!this.isGlobeRendering || terrain) {
// Mercator has terrain handling implemented properly and since terrain
// simply draws tile coordinates into a special framebuffer, this works well even for globe.
return this._mercatorTransform.screenPointToMercatorCoordinate(p, terrain);
Expand All @@ -964,7 +969,7 @@ export class GlobeTransform implements ITransform {
}

screenPointToLocation(p: Point, terrain?: Terrain): LngLat {
if (!this._globeRendering || terrain) {
if (!this.isGlobeRendering || terrain) {
// Mercator has terrain handling implemented properly and since terrain
// simply draws tile coordinates into a special framebuffer, this works well even for globe.
return this._mercatorTransform.screenPointToLocation(p, terrain);
Expand All @@ -973,7 +978,7 @@ export class GlobeTransform implements ITransform {
}

isPointOnMapSurface(p: Point, terrain?: Terrain): boolean {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.isPointOnMapSurface(p, terrain);
}

Expand Down Expand Up @@ -1012,7 +1017,7 @@ export class GlobeTransform implements ITransform {
* camera's position (not taking into account camera rotation at all).
*/
private isSurfacePointVisible(p: vec3): boolean {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return true;
}
const plane = this._cachedClippingPlane;
Expand Down Expand Up @@ -1148,7 +1153,7 @@ export class GlobeTransform implements ITransform {
}

getMatrixForModel(location: LngLatLike, altitude?: number): mat4 {
if (!this._globeRendering) {
if (!this.isGlobeRendering) {
return this._mercatorTransform.getMatrixForModel(location, altitude);
}
const lnglat = LngLat.convert(location);
Expand Down Expand Up @@ -1179,4 +1184,11 @@ export class GlobeTransform implements ITransform {
projectionData.fallbackMatrix = fallbackMatrixScaled;
return projectionData;
}

getFastPathSimpleProjectionMatrix(tileID: OverscaledTileID): mat4 {
if (!this.isGlobeRendering) {
return this._mercatorTransform.getFastPathSimpleProjectionMatrix(tileID);
}
return undefined;
}
}
36 changes: 30 additions & 6 deletions src/geo/projection/globe_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {clamp, lerp, mod, remapSaturate, wrap} from '../../util/util';
import {LngLat} from '../lng_lat';
import {MAX_VALID_LATITUDE, scaleZoom} from '../transform_helper';
import Point from '@mapbox/point-geometry';
import {tileCoordinatesToMercatorCoordinates} from './mercator_utils';
import {EXTENT} from '../../data/extent';

export function getGlobeCircumferencePixels(transform: {worldSize: number; center: {lat: number}}): number {
const radius = getGlobeRadiusPixels(transform.worldSize, transform.center.lat);
Expand Down Expand Up @@ -43,11 +43,35 @@ export function angularCoordinatesRadiansToVector(lngRadians: number, latRadians
return vec;
}

export function projectTileCoordinatesToSphere(inTileX: number, inTileY: number, tileID: {x: number; y: number; z: number}): vec3 {
const mercator = tileCoordinatesToMercatorCoordinates(inTileX, inTileY, tileID);
const angular = mercatorCoordinatesToAngularCoordinatesRadians(mercator.x, mercator.y);
const sphere = angularCoordinatesRadiansToVector(angular[0], angular[1]);
return sphere;
/**
* Projects a point within a tile to the surface of the unit sphere globe.
* @param inTileX - X coordinate inside the tile in range [0 .. 8192].
* @param inTileY - Y coordinate inside the tile in range [0 .. 8192].
* @param tileIdX - Tile's X coordinate in range [0 .. 2^zoom - 1].
* @param tileIdY - Tile's Y coordinate in range [0 .. 2^zoom - 1].
* @param tileIdZ - Tile's zoom.
* @returns A 3D vector - coordinates of the projected point on a unit sphere.
*/
export function projectTileCoordinatesToSphere(inTileX: number, inTileY: number, tileIdX: number, tileIdY: number, tileIdZ: number): vec3 {
// This code could be assembled from 3 fuctions, but this is a hot path for symbol placement,
// so for optimization purposes everything is inlined by hand.
//
// Non-inlined variant of this function would be this:
// const mercator = tileCoordinatesToMercatorCoordinates(inTileX, inTileY, tileID);
// const angular = mercatorCoordinatesToAngularCoordinatesRadians(mercator.x, mercator.y);
// const sphere = angularCoordinatesRadiansToVector(angular[0], angular[1]);
// return sphere;
const scale = 1.0 / (1 << tileIdZ);
const mercatorX = inTileX / EXTENT * scale + tileIdX * scale;
const mercatorY = inTileY / EXTENT * scale + tileIdY * scale;
const sphericalX = mod(mercatorX * Math.PI * 2.0 + Math.PI, Math.PI * 2);
const sphericalY = 2.0 * Math.atan(Math.exp(Math.PI - (mercatorY * Math.PI * 2.0))) - Math.PI * 0.5;
const len = Math.cos(sphericalY);
const vec = new Float64Array(3) as any;
vec[0] = Math.sin(sphericalX) * len;
vec[1] = Math.sin(sphericalY);
vec[2] = Math.cos(sphericalX) * len;
return vec;
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/geo/projection/mercator_transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ describe('transform', () => {
expect(fixedLngLat(transform.screenPointToLocation(new Point(250, 250)))).toEqual({lng: 0, lat: 0});
expect(fixedCoord(transform.screenPointToMercatorCoordinate(new Point(250, 250)))).toEqual({x: 0.5, y: 0.5, z: 0});
expect(transform.locationToScreenPoint(new LngLat(0, 0))).toEqual({x: 250, y: 250});
expect(transform.useGlobeControls).toBe(false);
});

test('does not throw on bad center', () => {
Expand Down Expand Up @@ -421,8 +420,8 @@ describe('transform', () => {
transform.setCenter(new LngLat(0.0, 0.0));

const customLayerMatrix = transform.getProjectionDataForCustomLayer().mainMatrix;
expect(customLayerMatrix[0].toString().length).toBeGreaterThan(10);
expect(transform.pixelsToClipSpaceMatrix[0].toString().length).toBeGreaterThan(10);
expect(customLayerMatrix[0].toString().length).toBeGreaterThan(9);
expect(transform.pixelsToClipSpaceMatrix[0].toString().length).toBeGreaterThan(9);
expect(transform.maxPitchScaleFactor()).toBeCloseTo(2.366025418080343, 5);
});

Expand Down
Loading

0 comments on commit 51bd024

Please sign in to comment.