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 covering tiles optimization #4937

Merged
merged 16 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
3 changes: 2 additions & 1 deletion src/geo/projection/covering_tiles.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {OverscaledTileID} from '../../source/tile_id';
import {Aabb, Frustum, IntersectionResult} from '../../util/primitives';
import {vec2, vec4} from 'gl-matrix';
import {IReadonlyTransform} from '../transform_interface';
import {MercatorCoordinate} from '../mercator_coordinate';
import {scaleZoom} from '../transform_helper';
import {clamp, degreesToRadians} from '../../util/util';
import {Terrain} from '../../render/terrain';
import {Frustum} from '../../util/primitives/frustum';
import {Aabb, IntersectionResult} from '../../util/primitives/aabb';

type CoveringTilesResult = {
tileID: OverscaledTileID;
Expand Down
2 changes: 1 addition & 1 deletion src/geo/projection/covering_tiles_details_provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Aabb} from '../../util/primitives';
import {Aabb} from '../../util/primitives/aabb';
import {MercatorCoordinate} from '../mercator_coordinate';
import {IReadonlyTransform} from '../transform_interface';
import {CoveringTilesOptions} from './covering_tiles';
Expand Down
140 changes: 1 addition & 139 deletions src/geo/projection/globe_covering_tiles.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Aabb} from '../../util/primitives';
import {Aabb} from '../../util/primitives/aabb';
import {expectToBeCloseToArray} from '../../util/test/util';
import {GlobeCoveringTilesDetailsProvider} from './globe_covering_tiles_details_provider';

Expand Down Expand Up @@ -53,141 +53,3 @@ describe('aabb creation', () => {
expectToBeCloseToArray([...aabb.max], [0, 1, 0.3985368153383868]);
});
});

describe('aabb cache', () => {
test('retains aabbs from last frame', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
const aabb1a = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
detailsProvider.newFrame();
const aabb1b = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
expect(aabb1a).toBe(aabb1b); // Test reference equality
});

test('clears no longer used aabbs', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
// Get 1+2+3
const box1a = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
const box2a = detailsProvider.getTileAABB({
x: 1,
y: 0,
z: 1,
}, null, null, null);
const box3a = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
detailsProvider.newFrame();
// Get 2+3+4
const box2b = detailsProvider.getTileAABB({
x: 1,
y: 0,
z: 1,
}, null, null, null);
const box3b = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
const box4b = detailsProvider.getTileAABB({
x: 1,
y: 1,
z: 1,
}, null, null, null);
detailsProvider.newFrame();
// Get 1+3+4
const box1c = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
const box3c = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
const box4c = detailsProvider.getTileAABB({
x: 1,
y: 1,
z: 1,
}, null, null, null);
// All returned objects should have equal internal values
expect(box1a).toEqual(box1c);
expect(box2a).toEqual(box2b);
expect(box3a).toEqual(box3b);
expect(box3a).toEqual(box3c);
expect(box4b).toEqual(box4c);
// Test that cache behaves as expected
expect(box1a).not.toBe(box1c);
expect(box2a).toBe(box2b);
expect(box3a).toBe(box3b);
expect(box3a).toBe(box3c);
expect(box4b).toBe(box4c);
});

test('does not clear cache if no new box was added', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
// Get 1+2+3
const box1a = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
const box2a = detailsProvider.getTileAABB({
x: 1,
y: 0,
z: 1,
}, null, null, null);
const box3a = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
detailsProvider.newFrame();
// Get 2+3
const box2b = detailsProvider.getTileAABB({
x: 1,
y: 0,
z: 1,
}, null, null, null);
const box3b = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
detailsProvider.newFrame();
// Get 1+3
const box1c = detailsProvider.getTileAABB({
x: 0,
y: 0,
z: 1,
}, null, null, null);
const box3c = detailsProvider.getTileAABB({
x: 0,
y: 1,
z: 1,
}, null, null, null);
// All returned objects should have equal internal values
expect(box1a).toEqual(box1c);
expect(box2a).toEqual(box2b);
expect(box3a).toEqual(box3b);
expect(box3a).toEqual(box3c);
// Test that cache behaves as expected
expect(box1a).toBe(box1c);
expect(box2a).toBe(box2b);
expect(box3a).toBe(box3b);
expect(box3a).toBe(box3c);
});
});
45 changes: 9 additions & 36 deletions src/geo/projection/globe_covering_tiles_details_provider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {vec3} from 'gl-matrix';
import {Aabb} from '../../util/primitives';
import {IReadonlyTransform} from '../transform_interface';
import {MercatorCoordinate} from '../mercator_coordinate';
import {EXTENT} from '../../data/extent';
import {projectTileCoordinatesToSphere} from './globe_utils';
import {CoveringTilesOptions, coveringZoomLevel} from './covering_tiles';
import {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
import {Aabb} from '../../util/primitives/aabb';
import {AabbCache} from '../../util/primitives/aabb_cache';

/**
* Computes distance of a point to a tile in an arbitrary axis.
Expand Down Expand Up @@ -38,25 +39,14 @@ function distanceToTileWrapX(pointX: number, pointY: number, tileCornerX: number
}

export class GlobeCoveringTilesDetailsProvider implements CoveringTilesDetailsProvider {
private _cachePrevious: Map<string, Aabb> = new Map();
private _cache: Map<string, Aabb> = new Map();
private _hadAnyChanges = false;
private _aabbCache: AabbCache = new AabbCache(this._computeTileAABB);

/**
* Prepares AABB cache for next frame. Call at the beginning of a frame.
* Any tile accesses in the last frame is kept in the cache, other tiles are deleted.
* Prepares the internal AABB cache for the next frame.
* @returns
*/
newFrame() {
if (!this._hadAnyChanges) {
// If no new boxes were added this frame, no need to conserve memory, do not clear caches.
return;
}
const oldCache = this._cachePrevious;
this._cachePrevious = this._cache;
this._cache = oldCache;
this._cache.clear();
this._hadAnyChanges = false;
this._aabbCache.newFrame();
}

/**
Expand Down Expand Up @@ -103,32 +93,15 @@ export class GlobeCoveringTilesDetailsProvider implements CoveringTilesDetailsPr
}
return 0;
}

/**
* Returns the AABB of the specified tile. The AABB is in the coordinate space where the globe is a unit sphere.
* @param tileID - Tile x, y and z for zoom.
*/
getTileAABB(tileID: {x: number; y: number; z: number}, wrap: number, elevation: number, options: CoveringTilesOptions): Aabb {
const key = `${tileID.z}_${tileID.x}_${tileID.y}`;
const cached = this._cache.get(key);
if (cached) {
return cached;
}
const cachedPrevious = this._cachePrevious.get(key);
if (cachedPrevious) {
this._cache.set(key, cachedPrevious);
return cachedPrevious;
}
const aabb = this._computeTileAABB(tileID, wrap, elevation, options);
this._cache.set(key, aabb);
this._hadAnyChanges = true;
return aabb;
}

allowVariableZoom(transform: IReadonlyTransform, options: CoveringTilesOptions): boolean {
return coveringZoomLevel(transform, options) > 4;
}

getTileAABB(tileID: { x: number; y: number; z: number }, wrap: number, elevation: number, options: CoveringTilesOptions) {
return this._aabbCache.getTileAABB(tileID, wrap, elevation, options);
}

private _computeTileAABB(tileID: {x: number; y: number; z: number}, _wrap: number, _elevation: number, _options: CoveringTilesOptions): Aabb {
// We can get away with only checking the 4 tile corners for AABB construction, because for any tile of zoom level 2 or higher
// it holds that the extremes (minimal or maximal value) of X, Y or Z coordinates must lie in one of the tile corners.
Expand Down
2 changes: 1 addition & 1 deletion src/geo/projection/globe_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {angularCoordinatesToSurfaceVector, getGlobeRadiusPixels, getZoomAdjustme
import {EXTENT} from '../../data/extent';
import type {ProjectionData, ProjectionDataParams} from './projection_data';
import {GlobeCoveringTilesDetailsProvider} from './globe_covering_tiles_details_provider';
import {Frustum} from '../../util/primitives';
import {Frustum} from '../../util/primitives/frustum';
import {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {OverscaledTileID} from '../../source/tile_id';
import {Aabb} from '../../util/primitives';
import {Aabb} from '../../util/primitives/aabb';
import {clamp} from '../../util/util';
import {MercatorCoordinate} from '../mercator_coordinate';
import {IReadonlyTransform} from '../transform_interface';
Expand Down
2 changes: 1 addition & 1 deletion src/geo/projection/mercator_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {EXTENT} from '../../data/extent';
import type {ProjectionData, ProjectionDataParams} from './projection_data';
import {scaleZoom, TransformHelper, zoomScale} from '../transform_helper';
import {MercatorCoveringTilesDetailsProvider} from './mercator_covering_tiles_details_provider';
import {Frustum} from '../../util/primitives';
import {Frustum} from '../../util/primitives/frustum';
import {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';

export class MercatorTransform implements ITransform {
Expand Down
2 changes: 1 addition & 1 deletion src/geo/transform_interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {PointProjection} from '../symbol/projection';
import {MapProjectionEvent} from '../ui/events';
import type {ProjectionData, ProjectionDataParams} from './projection/projection_data';
import {CoveringTilesDetailsProvider} from './projection/covering_tiles_details_provider';
import {Frustum} from '../util/primitives';
import {Frustum} from '../util/primitives/frustum';

export type TransformUpdateResult = {
forcePlacementUpdate?: boolean;
Expand Down
59 changes: 2 additions & 57 deletions src/util/primitives.ts → src/util/primitives/aabb.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,12 @@
import {mat4, vec3, vec4} from 'gl-matrix';
import {vec3, vec4} from 'gl-matrix';
import {type Frustum} from './frustum';

export const enum IntersectionResult {
None = 0,
Partial = 1,
Full = 2,
}

export class Frustum {

constructor(public points: vec4[], public planes: vec4[], public aabb: Aabb) { }

public static fromInvProjectionMatrix(invProj: mat4, worldSize: number = 1, zoom: number = 0): Frustum {
const clipSpaceCorners = [
[-1, 1, -1, 1],
[1, 1, -1, 1],
[1, -1, -1, 1],
[-1, -1, -1, 1],
[-1, 1, 1, 1],
[1, 1, 1, 1],
[1, -1, 1, 1],
[-1, -1, 1, 1]
];

const scale = Math.pow(2, zoom);

// Transform frustum corner points from clip space to tile space, Z to meters
const frustumCoords = clipSpaceCorners.map(v => {
v = vec4.transformMat4([] as any, v as any, invProj) as any;
const s = 1.0 / v[3] / worldSize * scale;
return vec4.mul(v as any, v as any, [s, s, 1.0 / v[3], s] as vec4);
});

const frustumPlanePointIndices = [
[0, 1, 2], // near
[6, 5, 4], // far
[0, 3, 7], // left
[2, 1, 5], // right
[3, 2, 6], // bottom
[0, 4, 5] // top
];

const frustumPlanes = frustumPlanePointIndices.map((p: number[]) => {
const a = vec3.sub([] as any, frustumCoords[p[0]] as vec3, frustumCoords[p[1]] as vec3);
const b = vec3.sub([] as any, frustumCoords[p[2]] as vec3, frustumCoords[p[1]] as vec3);
const n = vec3.normalize([] as any, vec3.cross([] as any, a, b)) as any;
const d = -vec3.dot(n, frustumCoords[p[1]] as vec3);
return n.concat(d);
});

const min: vec3 = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
const max: vec3 = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];

for (const p of frustumCoords) {
for (let i = 0; i < 3; i++) {
min[i] = Math.min(min[i], p[i]);
max[i] = Math.max(max[i], p[i]);
}
}

return new Frustum(frustumCoords, frustumPlanes, new Aabb(min, max));
}
}

export class Aabb {
min: vec3;
max: vec3;
Expand Down
Loading
Loading