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 - symbols & symbol bugfixes #4067

Merged
merged 22 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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): {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
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