Skip to content

Commit

Permalink
Globe: bugfixes: raster layer & projection change (#4546)
Browse files Browse the repository at this point in the history
* Port bugfix changes

* Update build size

* Fix render tests

* Add render test result for debian

* Increase raster tile granularity some more

* Adjust warped raster tile render test

* Add missing tsdoc param

* Use single checkerboard image for render test

* Globe examples now use setProjection

* Add new raster-pole render test image

* Add another raster-warped expected image

* Use "style.load" event on map instead of on style
  • Loading branch information
kubapelc authored Aug 13, 2024
1 parent 06c19e0 commit 7f3220f
Show file tree
Hide file tree
Showing 25 changed files with 158 additions and 58 deletions.
17 changes: 12 additions & 5 deletions src/geo/projection/globe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {easeCubicInOut, lerp} from '../../util/util';
import {mercatorYfromLat} from '../mercator_coordinate';
import {NORTH_POLE_Y, SOUTH_POLE_Y} from '../../render/subdivision';
import {SubdivisionGranularityExpression, SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import type {Projection, ProjectionGPUContext} from './projection';
import type {Projection, ProjectionGPUContext, TileMeshUsage} from './projection';
import {PreparedShader, shaders} from '../../shaders/shaders';
import {MercatorProjection} from './mercator';
import {ProjectionErrorMeasurement} from './globe_projection_error_measurement';
Expand All @@ -35,7 +35,9 @@ const granularitySettingsGlobe: SubdivisionGranularitySetting = new SubdivisionG
// otherwise they will be visibly warped at high zooms (before mercator transition).
// This si not needed on fill, because fill geometry tends to already be
// highly tessellated and granular at high zooms.
tile: new SubdivisionGranularityExpression(128, 16),
// Minimal granularity of 8 seems to be enough to avoid warped raster tiles, while also minimizing triangle count.
tile: new SubdivisionGranularityExpression(128, 32),
stencil: new SubdivisionGranularityExpression(128, 4),
circle: 3
});

Expand Down Expand Up @@ -172,9 +174,10 @@ export class GlobeProjection implements Projection {
return `${granularity.toString(36)}_${border ? 'b' : ''}${north ? 'n' : ''}${south ? 's' : ''}`;
}

public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean, allowPoles: boolean): Mesh {
public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean, allowPoles: boolean, usage: TileMeshUsage): Mesh {
// Stencil granularity must match fill granularity
const granularity = granularitySettingsGlobe.fill.getGranularityForZoomLevel(canonical.z);
const granularityConfig = usage === 'stencil' ? granularitySettingsGlobe.stencil : granularitySettingsGlobe.tile;
const granularity = granularityConfig.getGranularityForZoomLevel(canonical.z);
const north = (canonical.y === 0) && allowPoles;
const south = (canonical.y === (1 << canonical.z) - 1) && allowPoles;
return this._getMesh(context, granularity, hasBorder, north, south);
Expand Down Expand Up @@ -208,12 +211,16 @@ export class GlobeProjection implements Projection {
const quadsPerAxisX = granularity + (border ? 2 : 0); // two extra quads for border
const quadsPerAxisY = granularity + ((north || border) ? 1 : 0) + (south || border ? 1 : 0);
const verticesPerAxisX = quadsPerAxisX + 1; // one more vertex than quads
//const verticesPerAxisY = quadsPerAxisY + 1; // one more vertex than quads
const verticesPerAxisY = quadsPerAxisY + 1; // one more vertex than quads
const offsetX = border ? -1 : 0;
const offsetY = (border || north) ? -1 : 0;
const endX = granularity + (border ? 1 : 0);
const endY = granularity + ((border || south) ? 1 : 0);

if (verticesPerAxisX * verticesPerAxisY > (1 << 16)) {
throw new Error('Granularity is too large and meshes would not fit inside 16 bit vertex indices.');
}

const northY = NORTH_POLE_Y;
const southY = SOUTH_POLE_Y;

Expand Down
1 change: 1 addition & 0 deletions src/geo/projection/globe_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export class GlobeTransform implements ITransform {
type: 'projectiontransition',
newProjection: this._globeRendering ? 'globe' : 'globe-mercator',
},
forceSourceUpdate: true,
};
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/geo/projection/mercator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Projection, ProjectionGPUContext} from './projection';
import type {Projection, ProjectionGPUContext, TileMeshUsage} from './projection';
import type {CanonicalTileID} from '../../source/tile_id';
import {EXTENT} from '../../data/extent';
import {PreparedShader, shaders} from '../../shaders/shaders';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class MercatorProjection implements Projection {
// Do nothing.
}

public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean, _allowPoles: boolean): Mesh {
public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean, _allowPoles: boolean, _usage: TileMeshUsage): Mesh {
if (this._cachedMesh) {
return this._cachedMesh;
}
Expand Down
11 changes: 10 additions & 1 deletion src/geo/projection/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export type ProjectionGPUContext = {
useProgram: (name: string) => Program<any>;
};

/**
* @internal
* Specifies the usage for a square tile mesh:
* - 'stencil' for drawing stencil masks
* - 'raster' for drawing raster tiles, hillshade, etc.
*/
export type TileMeshUsage = 'stencil' | 'raster';

/**
* An interface the implementations of which are used internally by MapLibre to handle different projections.
*/
Expand Down Expand Up @@ -107,6 +115,7 @@ export interface Projection {
* @param tileID - The tile coordinates for which to return a mesh. Meshes for tiles that border the top/bottom mercator edge might include extra geometry for the north/south pole.
* @param hasBorder - When true, the mesh will also include a small border beyond the 0..EXTENT range.
* @param allowPoles - When true, the mesh will also include geometry to cover the north (south) pole, if the given tileID borders the mercator range's top (bottom) edge.
* @param usage - Specify the usage of the tile mesh, as different usages might use different levels of subdivision.
*/
getMeshFromTileID(context: Context, tileID: CanonicalTileID, hasBorder: boolean, allowPoles: boolean): Mesh;
getMeshFromTileID(context: Context, tileID: CanonicalTileID, hasBorder: boolean, allowPoles: boolean, usage: TileMeshUsage): Mesh;
}
1 change: 1 addition & 0 deletions src/geo/transform_interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type CoveringTilesOptions = CoveringZoomOptions & {
export type TransformUpdateResult = {
forcePlacementUpdate?: boolean;
fireProjectionEvent?: MapProjectionEvent;
forceSourceUpdate?: boolean;
};

export interface ITransformGetters {
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer
// and also enable stencil clipping. Make sure to render a proper tile clipping mask into stencil
// first though, as that doesn't seem to happen for background layers as of writing this.

const mesh = projection.getMeshFromTileID(context, tileID.canonical, false, true);
const mesh = projection.getMeshFromTileID(context, tileID.canonical, false, true, 'raster');
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, terrainData, projectionData, layer.id,
mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_hillshade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function renderHillshade(
if (!fbo) {
continue;
}
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true);
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, 'raster');

const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord);

Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_raster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function drawTiles(
const projectionData = transform.getProjectionData(coord, align);
const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners);

const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles);
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster');

const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled;

Expand Down
2 changes: 1 addition & 1 deletion src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export class Painter {
const stencilRef = tileStencilRefs[tileID.key];
const terrainData = this.style.map.terrain && this.style.map.terrain.getTerrainData(tileID);

const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true);
const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, useBorders, true, 'stencil');

const projectionData = transform.getProjectionData(tileID);

Expand Down
17 changes: 16 additions & 1 deletion src/render/subdivision_granularity_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export class SubdivisionGranularityExpression {
private readonly _minGranularity: number;

constructor(baseZoomGranularity: number, minGranularity: number) {
if (minGranularity > baseZoomGranularity) {
throw new Error('Min granularity must not be greater than base granularity.');
}

this._baseZoomGranularity = baseZoomGranularity;
this._minGranularity = minGranularity;
}
Expand All @@ -50,10 +54,15 @@ export class SubdivisionGranularitySetting {
public readonly line: SubdivisionGranularityExpression;

/**
* Granularity used for geometry covering the entire tile: stencil masks, raster tiles, etc.
* Granularity used for geometry covering the entire tile: raster tiles, etc.
*/
public readonly tile: SubdivisionGranularityExpression;

/**
* Granularity used for stencil masks for tiles.
*/
public readonly stencil: SubdivisionGranularityExpression;

/**
* Controls the granularity of `pitch-alignment: map` circles and heatmap kernels.
* More granular circles will more closely follow the map's surface.
Expand All @@ -73,6 +82,10 @@ export class SubdivisionGranularitySetting {
* Granularity used for geometry covering the entire tile: stencil masks, raster tiles, etc.
*/
tile: SubdivisionGranularityExpression;
/**
* Granularity used for stencil masks for tiles.
*/
stencil: SubdivisionGranularityExpression;
/**
* Controls the granularity of `pitch-alignment: map` circles and heatmap kernels.
* More granular circles will more closely follow the map's surface.
Expand All @@ -82,6 +95,7 @@ export class SubdivisionGranularitySetting {
this.fill = options.fill;
this.line = options.line;
this.tile = options.tile;
this.stencil = options.stencil;
this.circle = options.circle;
}

Expand All @@ -92,6 +106,7 @@ export class SubdivisionGranularitySetting {
fill: new SubdivisionGranularityExpression(0, 0),
line: new SubdivisionGranularityExpression(0, 0),
tile: new SubdivisionGranularityExpression(0, 0),
stencil: new SubdivisionGranularityExpression(0, 0),
circle: 1
});
}
5 changes: 3 additions & 2 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3092,10 +3092,12 @@ export class Map extends Camera {
this.style.update(parameters);
}

const transformUpdateResult = this.transform.newFrameUpdate();

// If we are in _render for any reason other than an in-progress paint
// transition, update source caches to check for and load any tiles we
// need for the current transform
if (this.style && this._sourcesDirty) {
if (this.style && (this._sourcesDirty || transformUpdateResult.forceSourceUpdate)) {
this._sourcesDirty = false;
this.style._updateSources(this.transform);
}
Expand All @@ -3112,7 +3114,6 @@ export class Map extends Camera {
this.transform.setElevation(0);
}

const transformUpdateResult = this.transform.newFrameUpdate();
this._placementDirty = this.style && this.style._updatePlacement(this.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, transformUpdateResult.forcePlacementUpdate);

if (transformUpdateResult.fireProjectionEvent) {
Expand Down
2 changes: 1 addition & 1 deletion src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function translatePosition(
/**
* Returns the signed distance between a point and a plane.
* @param plane - The plane equation, in the form where the first three components are the normal and the fourth component is the plane's distance from origin along normal.
* @param point - The point whose distance from plane is retunred.
* @param point - The point whose distance from plane is returned.
* @returns Signed distance of the point from the plane. Positive distances are in the half space where the plane normal points to, negative otherwise.
*/
export function pointPlaneSignedDistance(
Expand Down
2 changes: 1 addition & 1 deletion test/build/min.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('test min build', () => {
const decreaseQuota = 4096;

// feel free to update this value after you've checked that it has changed on purpose :-)
const expectedBytes = 871322;
const expectedBytes = 871711;

expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota);
expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota);
Expand Down
11 changes: 9 additions & 2 deletions test/examples/globe-fill-extrusion.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@
<script>
const map = new maplibregl.Map({
container: 'map',
projection: 'globe',
style: 'https://api.maptiler.com/maps/basic/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
zoom: 0,
center: [30.0, 40.0],
});

map.on('style.load', () => {
map.setProjection({
type: 'globe', // Set projection to globe
});
map.setSky({
'atmosphere-blend': 0.0, // Disable sky
});
});

map.on('load', () => {
map.addSource('extrude-polygons', {
'type': 'geojson',
Expand Down
13 changes: 10 additions & 3 deletions test/examples/globe-vector-tiles.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@
<script>
const map = new maplibregl.Map({
container: 'map',
projection: 'globe',
style: 'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
zoom: 0,
center: [137.9150899566626, 36.25956997955441],
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
});

map.on('style.load', () => {
map.setProjection({
type: 'globe', // Set projection to globe
});
map.setSky({
'atmosphere-blend': 0.0, // Disable sky
});
});
</script>
</body>
Expand Down
77 changes: 41 additions & 36 deletions test/examples/globe-zoom-planet-size-function.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,47 +35,52 @@
<br />
<button id="fly">Go to pole or equator</button>
<script>
fetch('https://demotiles.maplibre.org/style.json').then(response => response.json()).then(fetchedStyle => {
fetchedStyle['projection'] = {'type': 'globe'}; // Modify the fetched style before creating the map so that it already starts with globe projection.
fetchedStyle['sky'] = {'atmosphere-blend': 0.0}; // Disable sky.
const map = new maplibregl.Map({
container: 'map',
style: fetchedStyle,
center: [0, 0],
zoom: 2,
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});

map.on('style.load', () => {
map.setProjection({
type: 'globe', // Set projection to globe
});
map.setSky({
'atmosphere-blend': 0.0, // Disable sky
});
});

// To stay consistent with web mercator maps, globe is automatically enlarged when map center is nearing the poles.
// This keeps the map center visually similar to a mercator map with the same x,y and zoom.
// However, sometimes we want to negate this effect and keep the globe size consistent even when changing latitudes.
// This function computes what we need to add the the target zoom level when changing latitude.
function getZoomAdjustment(oldLatitude, newLatitude) {
return Math.log2(Math.cos(newLatitude / 180 * Math.PI) / Math.cos(oldLatitude / 180 * Math.PI));
}
// To stay consistent with web mercator maps, globe is automatically enlarged when map center is nearing the poles.
// This keeps the map center visually similar to a mercator map with the same x,y and zoom.
// However, sometimes we want to negate this effect and keep the globe size consistent even when changing latitudes.
// This function computes what we need to add the the target zoom level when changing latitude.
function getZoomAdjustment(oldLatitude, newLatitude) {
return Math.log2(Math.cos(newLatitude / 180 * Math.PI) / Math.cos(oldLatitude / 180 * Math.PI));
}

// Switch back and forth between zooming in and out.
let zoomIn = false;
const zoomDelta = 1.5;
// Switch back and forth between zooming in and out.
let zoomIn = false;
const zoomDelta = 1.5;

document.getElementById('fly').addEventListener('click', () => {
// Fly to a random location by offsetting the point -74.50, 40
// by up to 5 degrees.
const center = [
map.getCenter().lng,
zoomIn ? 0 : 80,
];
const mapZoom = map.getZoom();
const delta = (zoomIn ? zoomDelta : -zoomDelta);
// We want to change the map's globe size by some delta and change the center latitude at the same time,
// thus we need to compensate for the globe enlarging effect described earlier.
const zoom = map.getZoom() + delta + getZoomAdjustment(map.getCenter().lat, center[1]);
map.flyTo({
center,
zoom,
essential: true // this animation is considered essential with respect to prefers-reduced-motion
});
zoomIn = !zoomIn;
document.getElementById('fly').addEventListener('click', () => {
// Fly to a random location by offsetting the point -74.50, 40
// by up to 5 degrees.
const center = [
map.getCenter().lng,
zoomIn ? 0 : 80,
];
const mapZoom = map.getZoom();
const delta = (zoomIn ? zoomDelta : -zoomDelta);
// We want to change the map's globe size by some delta and change the center latitude at the same time,
// thus we need to compensate for the globe enlarging effect described earlier.
const zoom = map.getZoom() + delta + getZoomAdjustment(map.getCenter().lat, center[1]);
map.flyTo({
center,
zoom,
essential: true // this animation is considered essential with respect to prefers-reduced-motion
});
zoomIn = !zoomIn;
});
</script>
</body>
Expand Down
Binary file added test/integration/assets/tiles/checkerboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7f3220f

Please sign in to comment.