Skip to content

Commit

Permalink
Globe - symbols & symbol bugfixes (#4067)
Browse files Browse the repository at this point in the history
* Import changes from main vector globe branch

* Fix import

* Remove unused code

* Remove unused imports

* Update build size test

* Remove unused function

* Add render test results for Debian

* Add another Debian render test variant

* Add more render test variants

* Hide collision boxes on the backfacing side of the globe

* Fix pitch-aligned texts getting hidden when their anchor is beyond horizon

* Update build size

* Fix merge

* Better comment in draw_collision_debug

* Update build size

The 10 kb size increase seems to come from the main branch

* Minor refactor

* Use explicit types, even for unused parameters

* Refactor screenspace path projection

* Refactor imports for projection.ts and collision_index.ts

* Fix import in collision_index.ts
  • Loading branch information
kubapelc authored May 20, 2024
1 parent d2d8f75 commit 759606a
Show file tree
Hide file tree
Showing 76 changed files with 5,792 additions and 175 deletions.
15 changes: 9 additions & 6 deletions src/data/bucket/symbol_bucket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {StyleImage} from '../../style/style_image';
import glyphs from '../../../test/unit/assets/fontstack-glyphs.json' with {type: 'json'};
import {StyleGlyph} from '../../style/style_glyph';
import {MercatorProjection} from '../../geo/projection/mercator';
import {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';

// Load a point feature from fixture tile.
const vt = new VectorTile(new Protobuf(fs.readFileSync(path.resolve(__dirname, '../../../test/unit/assets/mbsv5-6-18-23.vector.pbf'))));
Expand Down Expand Up @@ -65,7 +66,6 @@ describe('SymbolBucket', () => {
const bucketA = bucketSetup() as any as SymbolBucket;
const bucketB = bucketSetup() as any as SymbolBucket;
const options = {iconDependencies: {}, glyphDependencies: {}} as PopulateParameters;
// HM TODO: this need to be fixed!!
const placement = new Placement(transform, new MercatorProjection(), undefined as any, 0, true);
const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
const crossTileSymbolIndex = new CrossTileSymbolIndex();
Expand All @@ -76,7 +76,8 @@ describe('SymbolBucket', () => {
{
bucket: bucketA,
glyphMap: stacks,
glyphPositions: {}
glyphPositions: {},
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
const tileA = new Tile(tileID, 512);
tileA.latestFeatureIndex = new FeatureIndex(tileID);
Expand All @@ -86,7 +87,7 @@ describe('SymbolBucket', () => {
// add same feature from bucket B
bucketB.populate([{feature} as IndexedFeature], options, undefined as any);
performSymbolLayout({
bucket: bucketB, glyphMap: stacks, glyphPositions: {}
bucket: bucketB, glyphMap: stacks, glyphPositions: {}, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
const tileB = new Tile(tileID, 512);
tileB.buckets = {test: bucketB};
Expand Down Expand Up @@ -124,7 +125,8 @@ describe('SymbolBucket', () => {
performSymbolLayout({
bucket,
glyphMap: stacks,
glyphPositions: {'Test': {97: fakeGlyph, 98: fakeGlyph, 99: fakeGlyph, 100: fakeGlyph, 101: fakeGlyph, 102: fakeGlyph} as any}
glyphPositions: {'Test': {97: fakeGlyph, 98: fakeGlyph, 99: fakeGlyph, 100: fakeGlyph, 101: fakeGlyph, 102: fakeGlyph} as any},
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);

expect(spy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -165,7 +167,8 @@ describe('SymbolBucket', () => {
expect(icons.b).toBe(true);

performSymbolLayout({
bucket, imageMap, imagePositions: imagePos
bucket, imageMap, imagePositions: imagePos,
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);

// undefined SDF should be treated the same as false SDF - no warning raised
Expand Down Expand Up @@ -206,7 +209,7 @@ describe('SymbolBucket', () => {
expect(icons.a).toBe(true);
expect(icons.b).toBe(true);

performSymbolLayout({bucket, imageMap, imagePositions: imagePos} as any);
performSymbolLayout({bucket, imageMap, imagePositions: imagePos, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision} as any);

// true SDF and false SDF in same bucket should trigger warning
expect(spy).toHaveBeenCalledTimes(1);
Expand Down
2 changes: 1 addition & 1 deletion src/data/bucket/symbol_bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ export class SymbolBucket implements Bucket {
}
}

addToLineVertexArray(anchor: Anchor, line: any) {
addToLineVertexArray(anchor: Anchor, line: Array<Point>) {
const lineStartIndex = this.lineVertexArray.length;
if (anchor.segment !== undefined) {
let sumForwardLength = anchor.dist(line[anchor.segment + 1]);
Expand Down
46 changes: 39 additions & 7 deletions src/geo/projection/globe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('GlobeProjection', () => {
describe('general plane properties', () => {
const mat = mat4.create();
const transform = createMockTransform({
pitchDegrees: 0,
pitch: 0,
});
globe.updateProjection(transform);
const projectionData = globe.getProjectionData({
Expand Down Expand Up @@ -79,6 +79,36 @@ describe('GlobeProjection', () => {
});
});
});

describe('projection', () => {
test('mercator coordinate to sphere point', () => {
const precisionDigits = 10;
const globe = new GlobeProjection();

let projectedAngles;
let projected;

projectedAngles = globe['_mercatorCoordinatesToAngularCoordinates'](0.5, 0.5);
expectToBeCloseToArray(projectedAngles, [0, 0], precisionDigits);
projected = globe['_angularCoordinatesToVector'](projectedAngles[0], projectedAngles[1]) as [number, number, number];
expectToBeCloseToArray(projected, [0, 0, 1], precisionDigits);

projectedAngles = globe['_mercatorCoordinatesToAngularCoordinates'](0, 0.5);
expectToBeCloseToArray(projectedAngles, [Math.PI, 0], precisionDigits);
projected = globe['_angularCoordinatesToVector'](projectedAngles[0], projectedAngles[1]) as [number, number, number];
expectToBeCloseToArray(projected, [0, 0, -1], precisionDigits);

projectedAngles = globe['_mercatorCoordinatesToAngularCoordinates'](0.75, 0.5);
expectToBeCloseToArray(projectedAngles, [Math.PI / 2.0, 0], precisionDigits);
projected = globe['_angularCoordinatesToVector'](projectedAngles[0], projectedAngles[1]) as [number, number, number];
expectToBeCloseToArray(projected, [1, 0, 0], precisionDigits);

projectedAngles = globe['_mercatorCoordinatesToAngularCoordinates'](0.5, 0);
expectToBeCloseToArray(projectedAngles, [0, 1.4844222297453324], precisionDigits); // ~0.47pi
projected = globe['_angularCoordinatesToVector'](projectedAngles[0], projectedAngles[1]) as [number, number, number];
expectToBeCloseToArray(projected, [0, 0.99627207622075, 0.08626673833405434], precisionDigits);
});
});
});

function testPlaneAgainstLngLat(lngDegrees: number, latDegrees: number, plane: Array<number>) {
Expand All @@ -102,21 +132,23 @@ function createMockTransform(object: {
latDegrees: number;
lngDegrees: number;
};
pitchDegrees?: number;
pitch?: number;
angleDegrees?: number;
width?: number;
height?: number;
bearing?: number;
}): TransformLike {
const pitchDegrees = object.pitchDegrees ? object.pitchDegrees : 0;
return {
center: new LngLat(
object.center ? (object.center.lngDegrees / 180.0 * Math.PI) : 0,
object.center ? (object.center.latDegrees / 180.0 * Math.PI) : 0),
worldSize: 10.5 * 512,
fov: 45.0,
width: 640,
height: 480,
width: object?.width || 640,
height: object?.height || 480,
cameraToCenterDistance: 759,
pitch: pitchDegrees, // in degrees
angle: object.angleDegrees ? (object.angleDegrees / 180.0 * Math.PI) : 0,
pitch: object?.pitch || 0, // in degrees
angle: -(object?.bearing || 0) / 180.0 * Math.PI,
zoom: 0,
invProjMatrix: null,
};
Expand Down
107 changes: 68 additions & 39 deletions src/geo/projection/globe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {SegmentVector} from '../../data/segment';
import posAttributes from '../../data/pos_attributes';
import type {Tile} from '../../source/tile';
import {browser} from '../../util/browser';
import {easeCubicInOut, lerp} from '../../util/util';
import {easeCubicInOut, lerp, mod} 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';
Expand All @@ -18,7 +18,7 @@ import type {Projection, ProjectionGPUContext, TransformLike} from './projection
import {PreparedShader, shaders} from '../../shaders/shaders';
import {MercatorProjection, translatePosition} from './mercator';
import {ProjectionErrorMeasurement} from './globe_projection_error_measurement';
import type {LngLat} from '../lng_lat';
import {LngLat, earthRadius} from '../lng_lat';

/**
* The size of border region for stencil masks, in internal tile coordinates.
Expand Down Expand Up @@ -327,12 +327,23 @@ export class GlobeProjection implements Projection {
}

/**
* Given a 2D point in the mercator base tile, returns its 3D coordinates on the surface of a unit sphere.
* Returns mercator coordinates in range 0..1 for given coordinates inside a tile and the tile's canonical ID.
*/
private _projectToSphere(mercatorX: number, mercatorY: number): vec3 {
const sphericalX = mercatorX * Math.PI * 2.0 + Math.PI;
private _tileCoordinatesToMercatorCoordinates(inTileX: number, inTileY: number, tileID: UnwrappedTileID): [number, number] {
const scale = 1.0 / (1 << tileID.canonical.z);
return [
inTileX / EXTENT * scale + tileID.canonical.x * scale,
inTileY / EXTENT * scale + tileID.canonical.y * scale
];
}

/**
* For given mercator coordinates in range 0..1, returns the angular coordinates on the sphere's surface, in radians.
*/
private _mercatorCoordinatesToAngularCoordinates(mercatorX: number, mercatorY: number): [number, number] {
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;
return this._angularCoordinatesToVector(sphericalX, sphericalY);
return [sphericalX, sphericalY];
}

private _angularCoordinatesToVector(lngRadians: number, latRadians: number): vec3 {
Expand All @@ -344,39 +355,39 @@ export class GlobeProjection implements Projection {
];
}

private _projectToSphereTile(inTileX: number, inTileY: number, unwrappedTileID: UnwrappedTileID): vec3 {
const scale = 1.0 / (1 << unwrappedTileID.canonical.z);
return this._projectToSphere(
inTileX / EXTENT * scale + unwrappedTileID.canonical.x * scale,
inTileY / EXTENT * scale + unwrappedTileID.canonical.y * scale
);
/**
* Given a 3D point on the surface of a unit sphere, returns its angular coordinates in degrees.
*/
private _sphereSurfacePointToCoordinates(surface: vec3): LngLat {
const latRadians = Math.asin(surface[1]);
const latDegrees = latRadians / Math.PI * 180.0;
const lengthXZ = Math.sqrt(surface[0] * surface[0] + surface[2] * surface[2]);
if (lengthXZ > 1e-6) {
const projX = surface[0] / lengthXZ;
const projZ = surface[2] / lengthXZ;
const acosZ = Math.acos(projZ);
const lngRadians = (projX > 0) ? acosZ : -acosZ;
const lngDegrees = lngRadians / Math.PI * 180.0;
return new LngLat(lngDegrees, latDegrees);
} else {
return new LngLat(0.0, latDegrees);
}
}

public isOccluded(x: number, y: number, unwrappedTileID: UnwrappedTileID): boolean {
const spherePos = this._projectToSphereTile(x, y, unwrappedTileID);

const plane = this._cachedClippingPlane;
// dot(position on sphere, occlusion plane equation)
const dotResult = plane[0] * spherePos[0] + plane[1] * spherePos[1] + plane[2] * spherePos[2] + plane[3];
return dotResult < 0.0;
private _projectTileCoordinatesToSphere(inTileX: number, inTileY: number, tileID: UnwrappedTileID): vec3 {
const mercator = this._tileCoordinatesToMercatorCoordinates(inTileX, inTileY, tileID);
const angular = this._mercatorCoordinatesToAngularCoordinates(mercator[0], mercator[1]);
const sphere = this._angularCoordinatesToVector(angular[0], angular[1]);
return sphere;
}

public project(x: number, y: number, unwrappedTileID: UnwrappedTileID) {
const spherePos = this._projectToSphereTile(x, y, unwrappedTileID);
const pos: vec4 = [spherePos[0], spherePos[1], spherePos[2], 1];
vec4.transformMat4(pos, pos, this._globeProjMatrixNoCorrection);
public isOccluded(x: number, y: number, unwrappedTileID: UnwrappedTileID): boolean {
const spherePos = this._projectTileCoordinatesToSphere(x, y, unwrappedTileID);

// Also check whether the point projects to the backfacing side of the sphere.
const plane = this._cachedClippingPlane;
// dot(position on sphere, occlusion plane equation)
const dotResult = plane[0] * spherePos[0] + plane[1] * spherePos[1] + plane[2] * spherePos[2] + plane[3];
const isOccluded = dotResult < 0.0;

return {
point: new Point(pos[0] / pos[3], pos[1] / pos[3]),
signedDistanceFromCamera: pos[3],
isOccluded
};
return dotResult < 0.0;
}

public transformLightDirection(transform: { center: LngLat }, dir: vec3): vec3 {
Expand Down Expand Up @@ -420,6 +431,15 @@ export class GlobeProjection implements Projection {
return Math.cos(transform.center.lat * Math.PI / 180);
}

public getPitchedTextCorrection(transform: { center: LngLat }, textAnchor: Point, tileID: UnwrappedTileID): number {
if (!this.useGlobeRendering) {
return 1.0;
}
const mercator = this._tileCoordinatesToMercatorCoordinates(textAnchor.x, textAnchor.y, tileID);
const angular = this._mercatorCoordinatesToAngularCoordinates(mercator[0], mercator[1]);
return this.getCircleRadiusCorrection(transform) / Math.cos(angular[1]);
}

private _updateAnimation(currentZoom: number) {
// Update globe transition animation
const globeState = this._globeProjectionOverride;
Expand Down Expand Up @@ -549,14 +569,23 @@ export class GlobeProjection implements Projection {
return mesh;
}

// HM TODO: fix this!
getPitchedTextCorrection(_transform: any, _anchor: any, _tile: any): number {
return 1.0;
}
public projectTileCoordinates(x: number, y: number, unwrappedTileID: UnwrappedTileID, getElevation: (x: number, y: number) => number) {
const spherePos = this._projectTileCoordinatesToSphere(x, y, unwrappedTileID);
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];
vec4.transformMat4(pos, pos, this._globeProjMatrixNoCorrection);

// HM TODO: fix this!
projectTileCoordinates(_x, _y, _t, _ele) {
// This function should only be used when useSpecialProjectionForSymbols is set to true.
throw new Error('Not implemented.');
// Also check whether the point projects to the backfacing side of the sphere.
const plane = this._cachedClippingPlane;
// dot(position on sphere, occlusion plane equation)
const dotResult = plane[0] * spherePos[0] + plane[1] * spherePos[1] + plane[2] * spherePos[2] + plane[3];
const isOccluded = dotResult < 0.0;

return {
point: new Point(pos[0] / pos[3], pos[1] / pos[3]),
signedDistanceFromCamera: pos[3],
isOccluded
};
}
}
4 changes: 2 additions & 2 deletions src/geo/projection/mercator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ describe('MercatorProjection', () => {
});
});

export function expectToBeCloseToArray(actual: Array<number>, expected: Array<number>) {
export function expectToBeCloseToArray(actual: Array<number>, expected: Array<number>, precision?: number) {
expect(actual).toHaveLength(expected.length);
for (let i = 0; i < expected.length; i++) {
expect(actual[i]).toBeCloseTo(expected[i]);
expect(actual[i]).toBeCloseTo(expected[i], precision);
}
}
36 changes: 15 additions & 21 deletions src/geo/projection/mercator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {mat4, vec3, vec4} from 'gl-matrix';
import type {Projection, ProjectionGPUContext} from './projection';
import type {Projection, ProjectionGPUContext, TransformLike} from './projection';
import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id';
import type Point from '@mapbox/point-geometry';
import Point from '@mapbox/point-geometry';
import type {Tile} from '../../source/tile';
import type {ProjectionData} from '../../render/program/projection_program';
import {pixelsToTileUnits} from '../../source/pixels_to_tile_units';
Expand All @@ -13,6 +13,7 @@ import {PosArray, TriangleIndexArray} from '../../data/array_types.g';
import {SegmentVector} from '../../data/segment';
import posAttributes from '../../data/pos_attributes';
import {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import type {LngLat} from '../lng_lat';

export const MercatorShaderDefine = '#define PROJECTION_MERCATOR';
export const MercatorShaderVariantKey = 'mercator';
Expand Down Expand Up @@ -117,28 +118,23 @@ export class MercatorProjection implements Projection {
return false;
}

public project(_x: number, _y: number, _unwrappedTileID: UnwrappedTileID): {
point: Point;
signedDistanceFromCamera: number;
isOccluded: boolean;
} {
// This function should only be used when useSpecialProjectionForSymbols is set to true.
throw new Error('Not implemented.');
public getPixelScale(_transform: { center: LngLat }): number {
return 1.0;
}

public getPixelScale(_: any): number {
public getCircleRadiusCorrection(_transform: { center: LngLat }): number {
return 1.0;
}

public getCircleRadiusCorrection(_: any): number {
public getPitchedTextCorrection(_transform: { center: LngLat }, _textAnchor: Point, _tileID: UnwrappedTileID): number {
return 1.0;
}

public translatePosition(transform: { angle: number; zoom: number }, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport'): [number, number] {
public translatePosition(transform: TransformLike, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport'): [number, number] {
return translatePosition(transform, tile, translate, translateAnchor);
}

public getMeshFromTileID(context: Context, _: CanonicalTileID, _hasBorder: boolean): Mesh {
public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean): Mesh {
if (this._cachedMesh) {
return this._cachedMesh;
}
Expand All @@ -162,17 +158,15 @@ export class MercatorProjection implements Projection {
return this._cachedMesh;
}

public transformLightDirection(_: any, dir: vec3): vec3 {
public transformLightDirection(_transform: { center: LngLat }, dir: vec3): vec3 {
return vec3.clone(dir);
}

// HM TODO: fix this!
getPitchedTextCorrection(_transform: any, _anchor: any, _tile: any) {
return 1;
}

// HM TODO: fix this!
projectTileCoordinates(_x, _y, _t, _ele) {
public projectTileCoordinates(_x: number, _y: number, _unwrappedTileID: UnwrappedTileID, _getElevation: (x: number, y: number) => number): {
point: Point;
signedDistanceFromCamera: number;
isOccluded: boolean;
} {
// This function should only be used when useSpecialProjectionForSymbols is set to true.
throw new Error('Not implemented.');
}
Expand Down
Loading

0 comments on commit 759606a

Please sign in to comment.