Skip to content

Commit

Permalink
Globe occlusion for popups and improving marker performance on globe (#…
Browse files Browse the repository at this point in the history
…11658)

* Refactoring and impproving globe occlusion

* Popups are occluded behind globe

* Updating test and reducing precision

* Support terrain + globe together and code organization

* Changing error message to reflect that Globe supports terrain

* Adding popup occlusion test

Co-authored-by: Karim Naaji <[email protected]>
  • Loading branch information
SnailBones and karimnaaji authored Apr 26, 2022
1 parent c40038a commit 6b5ff72
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 67 deletions.
35 changes: 20 additions & 15 deletions src/geo/projection/globe_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {TriangleIndexArray, GlobeVertexArray, LineIndexArray, PosArray} from '..
import {Aabb} from '../../util/primitives.js';
import LngLatBounds from '../lng_lat_bounds.js';

import type LngLat from '../lng_lat.js';
import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js';
import type Context from '../../gl/context.js';
import type {Vec3, Mat4} from 'gl-matrix';
Expand All @@ -26,6 +27,9 @@ import type VertexBuffer from '../../gl/vertex_buffer.js';
import type Transform from '../transform.js';
import Point from '@mapbox/point-geometry';

export const GLOBE_ZOOM_THRESHOLD_MIN = 5;
export const GLOBE_ZOOM_THRESHOLD_MAX = 6;

export const GLOBE_RADIUS = EXTENT / Math.PI / 2.0;
const GLOBE_METERS_TO_ECEF = mercatorZfromAltitude(1, 0.0) * 2.0 * GLOBE_RADIUS * Math.PI;
const GLOBE_NORMALIZATION_BIT_RANGE = 15;
Expand Down Expand Up @@ -365,9 +369,6 @@ export function calculateGlobeMercatorMatrix(tr: Transform): Float32Array {
return Float32Array.from(posMatrix);
}

export const GLOBE_ZOOM_THRESHOLD_MIN = 5;
export const GLOBE_ZOOM_THRESHOLD_MAX = 6;

export function globeToMercatorTransition(zoom: number): number {
return smoothstep(GLOBE_ZOOM_THRESHOLD_MIN, GLOBE_ZOOM_THRESHOLD_MAX, zoom);
}
Expand Down Expand Up @@ -412,9 +413,13 @@ export function getLatitudinalLod(lat: number): number {
return lod;
}

const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);
export function globeCenterToScreenPoint(tr: Transform): Point {
const pos = [0, 0, 0];
const matrix = mat4.identity(new Float64Array(16));
mat4.multiply(matrix, tr.pixelMatrix, tr.globeMatrix);
vec3.transformMat4(pos, pos, matrix);
return new Point(pos[0], pos[1]);
}

function cameraPositionInECEF(tr: Transform): Array<number> {
// Here "center" is the center of the globe. We refer to transform._center
Expand All @@ -439,24 +444,24 @@ function cameraPositionInECEF(tr: Transform): Array<number> {
return vec3.add([], centerToPivot, pivotToCamera);
}

// Return the angle of the normal vector of the sphere relative to the camera at a screen point.
// Return the angle of the normal vector of the sphere relative to the camera.
// i.e. how much to tilt map-aligned markers.
export function globeTiltAtScreenPoint(tr: Transform, point: Point): number {
const lngLat = tr.pointLocation(point);
export function globeTiltAtLngLat(tr: Transform, lngLat: LngLat): number {
const centerToPoint = latLngToECEF(lngLat.lat, lngLat.lng);
const centerToCamera = cameraPositionInECEF(tr);
const pointToCamera = vec3.subtract([], centerToCamera, centerToPoint);
return vec3.angle(pointToCamera, centerToPoint);
}

export function globeCenterToScreenPoint(tr: Transform): Point {
const pos = [0, 0, 0];
const matrix = mat4.identity(new Float64Array(16));
mat4.multiply(matrix, tr.pixelMatrix, tr.globeMatrix);
vec3.transformMat4(pos, pos, matrix);
return new Point(pos[0], pos[1]);
export function isLngLatBehindGlobe(tr: Transform, lngLat: LngLat): boolean {
// We consider 1% past the horizon not occluded, this allows popups to be dragged around the globe edge without fading.
return (globeTiltAtLngLat(tr, lngLat) > Math.PI / 2 * 1.01);
}

const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);

export class GlobeSharedBuffers {
_poleNorthVertexBuffer: VertexBuffer;
_poleSouthVertexBuffer: VertexBuffer;
Expand Down
2 changes: 1 addition & 1 deletion src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1921,7 +1921,7 @@ class Transform {
_terrainEnabled(): boolean {
if (!this._elevation) return false;
if (!this.projection.supportsTerrain) {
warnOnce('Terrain is not yet supported with alternate projections. Use mercator to enable terrain.');
warnOnce('Terrain is not yet supported with alternate projections. Use mercator or globe to enable terrain.');
return false;
}
return true;
Expand Down
45 changes: 20 additions & 25 deletions src/ui/marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type Popup from './popup.js';
import type {LngLatLike} from "../geo/lng_lat.js";
import type {MapMouseEvent, MapTouchEvent} from './events.js';
import type {PointLike} from '@mapbox/point-geometry';
import {globeTiltAtScreenPoint, globeCenterToScreenPoint} from '../geo/projection/globe_util.js';
import {globeTiltAtLngLat, globeCenterToScreenPoint, isLngLatBehindGlobe} from '../geo/projection/globe_util.js';

type Options = {
element?: HTMLElement,
Expand Down Expand Up @@ -333,6 +333,7 @@ export default class Marker extends Evented {
} : this._offset;
}
this._popup = popup;
popup._marker = this;
if (this._lngLat) this._popup.setLngLat(this._lngLat);

this._element.setAttribute('role', 'button');
Expand Down Expand Up @@ -410,20 +411,17 @@ export default class Marker extends Evented {
return this;
}

_occluded(unprojected: LngLat): boolean {
_behindTerrain(): boolean {
const map = this._map;
if (!map) return false;
const unprojected = map.unproject(this._pos);
const camera = map.getFreeCameraOptions();
if (camera.position) {
const cameraLngLat = camera.position.toLngLat();
const shortestDistance = cameraLngLat.distanceTo(unprojected);
const distanceToMarker = cameraLngLat.distanceTo(this._lngLat);
// In globe view, we only occlude if past ~100 km from cameraLngLat (i.e. screen center).
// This fixes an issue where a marker at screen center results in very small distances,
// with the error introduced from `map.unproject(pos)` occasionally causing the marker to be incorrectly occluded.
return shortestDistance < distanceToMarker * 0.9 && (!map._usingGlobe() || distanceToMarker > 100000);
}
return false;
if (!camera.position) return false;
const cameraLngLat = camera.position.toLngLat();
const toClosestSurface = cameraLngLat.distanceTo(unprojected);
const toMarker = cameraLngLat.distanceTo(this._lngLat);
return toClosestSurface < toMarker * 0.9;

}

_evaluateOpacity() {
Expand All @@ -437,22 +435,19 @@ export default class Marker extends Evented {
return;
}
const mapLocation = map.unproject(pos);
let opacity = 1;
if (map._usingGlobe()) {
opacity = this._occluded(mapLocation) ? 0 : 1;
} else if (map.transform._terrainEnabled() && map.getTerrain()) {
opacity = this._occluded(mapLocation) ? TERRAIN_OCCLUDED_OPACITY : 1;
let opacity;
if (map._usingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) {
opacity = 0;
} else {
opacity = 1 - map._queryFogOpacity(mapLocation);
if (map.transform._terrainEnabled() && map.getTerrain() && this._behindTerrain()) {
opacity *= TERRAIN_OCCLUDED_OPACITY;
}
}

const fogOpacity = map._queryFogOpacity(mapLocation);
opacity *= (1.0 - fogOpacity);
const pointerEvents = opacity ? 'auto' : 'none';

this._element.style.opacity = `${opacity}`;
this._element.style.pointerEvents = pointerEvents;
this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none';
if (this._popup) {
const container = this._popup._container;
if (container) { container.style.pointerEvents = pointerEvents; }
this._popup._setOpacity(opacity);
}

Expand Down Expand Up @@ -490,7 +485,7 @@ export default class Marker extends Evented {
const pitch = map.getPitch();
return pitch ? `rotateX(${pitch}deg)` : '';
}
const tilt = radToDeg(globeTiltAtScreenPoint(map.transform, pos));
const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat));
const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform));
const tiltOverDist = tilt / (Math.abs(posFromCenter.x) + Math.abs(posFromCenter.y));
const yTilt = posFromCenter.x * tiltOverDist;
Expand Down
8 changes: 8 additions & 0 deletions src/ui/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import Point from '@mapbox/point-geometry';
import window from '../util/window.js';
import smartWrap from '../util/smart_wrap.js';
import {type Anchor, anchorTranslate} from './anchor.js';
import {isLngLatBehindGlobe} from '../geo/projection/globe_util.js';

import type Map from './map.js';
import type {LngLatLike} from '../geo/lng_lat.js';
import type {PointLike} from '@mapbox/point-geometry';
import type Marker from './marker.js';

const defaultOptions = {
closeButton: true,
Expand Down Expand Up @@ -111,6 +113,7 @@ export default class Popup extends Evented {
_pos: ?Point;
_anchor: Anchor;
_classList: Set<string>;
_marker: ?Marker;

constructor(options: PopupOptions) {
super();
Expand Down Expand Up @@ -625,6 +628,11 @@ export default class Popup extends Evented {
});
}

if (!this._marker && map._usingGlobe()) {
const opacity = isLngLatBehindGlobe(map.transform, this._lngLat) ? 0 : 1;
this._setOpacity(opacity);
}

this._updateClassList();
}

Expand Down
53 changes: 27 additions & 26 deletions test/unit/ui/marker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1002,14 +1002,14 @@ test('Globe', (t) => {
});
});

test('Marker is positioned on globe surface', (t) => {
test('Marker is positioned on globe edge', (t) => {
const map = createMap(t);
const marker = new Marker()
.setLngLat([90, 0])
.setLngLat([85, 0])
.addTo(map);
map._domRenderTaskQueue.run();

t.match(marker.getElement().style.transform, " translate(384px,256px)");
t.match(marker.getElement().style.transform, " translate(377px,256px)");
map.setProjection('globe');
map.once('render', () => {
t.match(marker.getElement().style.transform, "translate(330px,256px)");
Expand Down Expand Up @@ -1043,7 +1043,7 @@ test('Globe', (t) => {
function rotation(marker, dimension) {
const transform = marker.getElement().style.transform;
const reg = new RegExp(`rotate${dimension}\\(([-.e0-9]+)deg\\)`);
return +Number.parseFloat(transform.match(reg)[1]).toFixed(9);
return +Number.parseFloat(transform.match(reg)[1]).toFixed();
}

test('Globe with pitchAlignment and rotationAlingment: map, changing longitude', (t) => {
Expand All @@ -1058,11 +1058,12 @@ test('Globe', (t) => {
t.notMatch(transform(marker), "rotateX");
t.notMatch(transform(marker), "rotateZ");

marker.setLngLat([90, 0]);
marker.setLngLat([84, 0]);
map.once('render', () => {
t.match(transform(marker), "translate(330px,256px)");
t.same(rotation(marker, "X"), 0);
t.same(rotation(marker, "Y"), 88.975673489);
t.same(rotation(marker, "Y"), 90);
t.same(marker.getElement().style.opacity, 1.0);
map.remove();
t.end();
});
Expand All @@ -1072,20 +1073,20 @@ test('Globe', (t) => {
const map = createMap(t);
map.setProjection('globe');
const marker = new Marker({rotationAlignment: 'map', pitchAlignment: 'map'})
.setLngLat([0, 89])
.setLngLat([0, 84])
.addTo(map);
map._domRenderTaskQueue.run();

t.match(transform(marker), "translate(256px,182px)");
t.same(rotation(marker, "X"), 88.975673489);
t.same(rotation(marker, "X"), 90);
t.same(rotation(marker, "Y"), 0);

marker.setLngLat([-45, 45]);
map.on('render', () => {
t.match(transform(marker), "translate(217px,201px)");
t.same(rotation(marker, "X"), 38.465875602);
t.same(rotation(marker, "Y"), -27.2758027);
t.same(rotation(marker, "Z"), 38.030140844);
t.same(rotation(marker, "X"), 38);
t.same(rotation(marker, "Y"), -27);
t.same(rotation(marker, "Z"), 38);
map.remove();
t.end();
});
Expand Down Expand Up @@ -1114,19 +1115,19 @@ test('Globe', (t) => {
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m2), "translate(256px,200px)");
t.same(rotation(m2, "X"), 49.299382704);
t.same(rotation(m2, "X"), 49);
t.same(rotation(m2, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m3), "translate(256px,296px)");
t.same(rotation(m3, "X"), -32.835045377);
t.same(rotation(m3, "X"), -33);
t.same(rotation(m3, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m4), "translate(295px,311px)");
t.same(rotation(m4, "X"), -38.465875602);
t.same(rotation(m4, "Y"), 27.2758027);
t.same(rotation(m4, "Z"), 38.030140843);
t.same(rotation(m4, "X"), -38);
t.same(rotation(m4, "Y"), 27);
t.same(rotation(m4, "Z"), 38);

map.setPitch(45);
map.once('render', () => {
Expand All @@ -1136,19 +1137,19 @@ test('Globe', (t) => {
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m2), "translate(256px,234px)");
t.same(rotation(m2, "X"), 85.512269796);
t.same(rotation(m2, "X"), 92);
t.same(rotation(m2, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m3), "translate(256px,294px)");
t.same(rotation(m3, "X"), 11.861002077);
t.same(rotation(m3, "X"), 12);
t.same(rotation(m3, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m4), "translate(297px,327px)");
t.same(rotation(m4, "X"), -10.677882737);
t.same(rotation(m4, "Y"), 25.158956455);
t.same(rotation(m4, "Z"), 29.578463693);
t.same(rotation(m4, "X"), -11);
t.same(rotation(m4, "Y"), 25);
t.same(rotation(m4, "Z"), 30);

map.setPitch(30);
map.once('render', () => {
Expand All @@ -1158,19 +1159,19 @@ test('Globe', (t) => {
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m2), "translate(256px,220px)");
t.same(rotation(m2, "X"), 78.903346373);
t.same(rotation(m2, "X"), 78);
t.same(rotation(m2, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m3), "translate(256px,297px)");
t.same(rotation(m3, "X"), -2.826321362);
t.same(rotation(m3, "X"), -3);
t.same(rotation(m3, "Y"), 0);
t.notMatch(transform(m1), "rotateZ");

t.match(transform(m4), "translate(296px,326px)");
t.same(rotation(m4, "X"), -19.579260926);
t.same(rotation(m4, "Y"), 23.961063055);
t.same(rotation(m4, "Z"), 30.522423436);
t.same(rotation(m4, "X"), -20);
t.same(rotation(m4, "Y"), 24);
t.same(rotation(m4, "Z"), 31);

map.remove();
t.end();
Expand Down
26 changes: 26 additions & 0 deletions test/unit/ui/popup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,32 @@ test('Popup offset can be set via setOffset', (t) => {
t.end();
});

test('Popup is positioned and occluded correctly on globe', (t) => {
const map = createMap(t, {width: 1024});
map.setProjection('globe');

const popup = new Popup()
.setLngLat([45, 0])
.setText('Test')
.addTo(map);

t.same(popup._pos, map.project([45, 0]));
t.same(popup._content.style.opacity, 1);
t.same(popup._content.style.pointerEvents, 'auto');

popup.setLngLat([270, 0]);
t.same(popup._pos, map.project([270, 0]));
t.same(popup._content.style.opacity, 0);
t.same(popup._content.style.pointerEvents, 'none');

popup.setLngLat([0, 45]);
t.same(popup._pos, map.project([0, 45]));
t.same(popup._content.style.opacity, 1);
t.same(popup._content.style.pointerEvents, 'auto');

t.end();
});

test('Popup can be removed and added again (#1477)', (t) => {
const map = createMap(t);

Expand Down

0 comments on commit 6b5ff72

Please sign in to comment.