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

Globe: symbol and coveringTiles optimizations #4778

Merged
merged 13 commits into from
Oct 15, 2024
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 {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
// 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