Skip to content

Commit

Permalink
Symbol projection optimization (#4599)
Browse files Browse the repository at this point in the history
* Add projection fast path for common symbols

* project() accepts two numbers instead of the point class

* Revert/optimize projectAndGetPerspectiveRatio to more closely resemble its original state

* Update build size

* Add a changelog entry
  • Loading branch information
kubapelc authored Sep 3, 2024
1 parent 1955ee6 commit c6a3d09
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 46 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ using `transformCameraUpdate` caused the `maxBounds` to stop working just for ea
- Fix Map#off to not remove listener with layer(s) registered with Map#once ([#4592](https://github.com/maplibre/maplibre-gl-js/pull/4592))
- Improve types a bit for `addSource` and `getSource` ([#4616](https://github.com/maplibre/maplibre-gl-js/pull/4616))
- Fix the color near the horizon when terrain is enabled without any sky ([#4607](https://github.com/maplibre/maplibre-gl-js/pull/4607))
- Fix bug where `fitBounds` and `cameraForBounds` would not display accross the 180th meridian (antimeridian)
- Fix bug where `fitBounds` and `cameraForBounds` would not display across the 180th meridian (antimeridian)
- Fix white flickering on map resize ([#4158](https://github.com/maplibre/maplibre-gl-js/pull/4158))
- Fixed a performance regression related to symbol placement ([#4599](https://github.com/maplibre/maplibre-gl-js/pull/4599))
- _...Add new stuff here..._

## 4.6.0
Expand Down
4 changes: 2 additions & 2 deletions src/render/draw_symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function getShiftedAnchor(projectedAnchorPoint: Point, projectionContext: symbol
adjustedShift = adjustedShift.rotate(-transformAngle);
}
const tileAnchorShifted = translatedAnchor.add(adjustedShift);
return symbolProjection.project(tileAnchorShifted, projectionContext.labelPlaneMatrix, projectionContext.getElevation).point;
return symbolProjection.project(tileAnchorShifted.x, tileAnchorShifted.y, projectionContext.labelPlaneMatrix, projectionContext.getElevation).point;
} else {
if (rotateWithMap) {
// Compute the angle with which to rotate the anchor, so that it is aligned with
Expand Down Expand Up @@ -231,7 +231,7 @@ function updateVariableAnchorsForBucket(
unwrappedTileID
};
const projectedAnchor = pitchWithMap ?
symbolProjection.project(tileAnchor, posMatrix, getElevation) :
symbolProjection.project(tileAnchor.x, tileAnchor.y, posMatrix, getElevation) :
symbolProjection.projectTileCoordinatesToViewport(tileAnchor.x, tileAnchor.y, projectionContext);
const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.cameraToCenterDistance, projectedAnchor.signedDistanceFromCamera);
let renderTextSize = evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / ONE_EM;
Expand Down
92 changes: 61 additions & 31 deletions src/symbol/collision_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {PathInterpolator} from './path_interpolator';

import * as intersectionTests from '../util/intersection_tests';
import {GridIndex} from './grid_index';
import {mat4} from 'gl-matrix';
import {mat4, vec4} from 'gl-matrix';
import ONE_EM from '../symbol/one_em';

import * as projection from '../symbol/projection';
Expand Down Expand Up @@ -48,6 +48,14 @@ export type FeatureKey = {
overlapMode: OverlapMode;
};

type ProjectedBox = {
/**
* The AABB in the format [minX, minY, maxX, maxY].
*/
box: [number, number, number, number];
allPointsOccluded: boolean;
};

/**
* @internal
* A collision index used to prevent symbols from overlapping. It keep tracks of
Expand Down Expand Up @@ -118,18 +126,37 @@ export class CollisionIndex {
getElevation
);

const projectedBox = this._projectCollisionBox(
collisionBox,
textPixelRatio,
posMatrix,
unwrappedTileID,
pitchWithMap,
rotateWithMap,
translation,
projectedPoint,
getElevation,
shift
);
const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;

let projectedBox: ProjectedBox;

if (!pitchWithMap && !rotateWithMap) {
// Fast path for common symbols
const pointX = projectedPoint.point.x + (shift ? shift.x * tileToViewport : 0);
const pointY = projectedPoint.point.y + (shift ? shift.y * tileToViewport : 0);
projectedBox = {
allPointsOccluded: false,
box: [
pointX + collisionBox.x1 * tileToViewport,
pointY + collisionBox.y1 * tileToViewport,
pointX + collisionBox.x2 * tileToViewport,
pointY + collisionBox.y2 * tileToViewport,
]
};
} else {
projectedBox = this._projectCollisionBox(
collisionBox,
tileToViewport,
posMatrix,
unwrappedTileID,
pitchWithMap,
rotateWithMap,
translation,
projectedPoint,
getElevation,
shift
);
}

const [tlX, tlY, brX, brY] = projectedBox.box;

Expand Down Expand Up @@ -323,7 +350,7 @@ export class CollisionIndex {
}

projectPathToScreenSpace(projectedPath: Array<Point>, projectionContext: SymbolProjectionContext, labelToScreenMatrix: mat4) {
return projectedPath.map(p => projection.project(p, labelToScreenMatrix, projectionContext.getElevation));
return projectedPath.map(p => projection.project(p.x, p.y, labelToScreenMatrix, projectionContext.getElevation));
}

/**
Expand Down Expand Up @@ -407,29 +434,37 @@ export class CollisionIndex {
}
}

projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number, unwrappedTileID: UnwrappedTileID, getElevation?: (x: number, y: number) => number) {
const projected = this.mapProjection.useSpecialProjectionForSymbols ?
this.mapProjection.projectTileCoordinates(x, y, unwrappedTileID, getElevation) :
projection.project(new Point(x, y), posMatrix, getElevation);
projectAndGetPerspectiveRatio(posMatrix: mat4, x: number, y: number, _unwrappedTileID: UnwrappedTileID, getElevation?: (x: number, y: number) => number) {
// The code here is duplicated from "projection.ts" for performance.
// Code here is subject to change once globe is merged.
let pos;
if (getElevation) { // slow because of handle z-index
pos = [x, y, getElevation(x, y), 1] as vec4;
vec4.transformMat4(pos, pos, posMatrix);
} else { // fast because of ignore z-index
pos = [x, y, 0, 1] as vec4;
projection.xyTransformMat4(pos, pos, posMatrix);
}
const w = pos[3];
return {
point: new Point(
(((projected.point.x + 1) / 2) * this.transform.width) + viewportPadding,
(((-projected.point.y + 1) / 2) * this.transform.height) + viewportPadding
(((pos[0] / w + 1) / 2) * this.transform.width) + viewportPadding,
(((-pos[1] / w + 1) / 2) * this.transform.height) + viewportPadding
),
// See perspective ratio comment in symbol_sdf.vertex
// We're doing collision detection in viewport space so we need
// to scale down boxes in the distance
perspectiveRatio: 0.5 + 0.5 * (this.transform.cameraToCenterDistance / projected.signedDistanceFromCamera),
isOccluded: projected.isOccluded,
signedDistanceFromCamera: projected.signedDistanceFromCamera
perspectiveRatio: 0.5 + 0.5 * (this.transform.cameraToCenterDistance / w),
isOccluded: false,
signedDistanceFromCamera: w
};
}

getPerspectiveRatio(posMatrix: mat4, x: number, y: number, unwrappedTileID: UnwrappedTileID, getElevation?: (x: number, y: number) => number): number {
// We don't care about the actual projected point, just its W component.
const projected = this.mapProjection.useSpecialProjectionForSymbols ?
this.mapProjection.projectTileCoordinates(x, y, unwrappedTileID, getElevation) :
projection.project(new Point(x, y), posMatrix, getElevation);
projection.project(x, y, posMatrix, getElevation);
return 0.5 + 0.5 * (this.transform.cameraToCenterDistance / projected.signedDistanceFromCamera);
}

Expand Down Expand Up @@ -457,7 +492,7 @@ export class CollisionIndex {
*/
private _projectCollisionBox(
collisionBox: SingleCollisionBox,
textPixelRatio: number,
tileToViewport: number,
posMatrix: mat4,
unwrappedTileID: UnwrappedTileID,
pitchWithMap: boolean,
Expand All @@ -466,12 +501,7 @@ export class CollisionIndex {
projectedPoint: {point: Point; perspectiveRatio: number; signedDistanceFromCamera: number},
getElevation?: (x: number, y: number) => number,
shift?: Point
): {
box: [number, number, number, number];
allPointsOccluded: boolean;
} {

const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
): ProjectedBox {

// These vectors are valid both for screen space viewport-rotation-aligned texts and for pitch-align: map texts that are map-rotation-aligned.
let vecEast = new Point(1, 0);
Expand Down
2 changes: 1 addition & 1 deletion src/symbol/projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Projection', () => {
test('matrix float precision', () => {
const point = new Point(10.000000005, 0);
const matrix = mat4.create();
expect(project(point, matrix).point.x).toBeCloseTo(point.x, 10);
expect(project(point.x, point.y, matrix).point.x).toBeCloseTo(point.x, 10);
});
});

Expand Down
20 changes: 10 additions & 10 deletions src/symbol/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ function getGlCoordMatrix(posMatrix: mat4,
}
}

function project(point: Point, matrix: mat4, getElevation?: (x: number, y: number) => number): PointProjection {
function project(x: number, y: number, matrix: mat4, getElevation?: (x: number, y: number) => number): PointProjection {
let pos;
if (getElevation) { // slow because of handle z-index
pos = [point.x, point.y, getElevation(point.x, point.y), 1] as vec4;
pos = [x, y, getElevation(x, y), 1] as vec4;
vec4.transformMat4(pos, pos, matrix);
} else { // fast because of ignore z-index
pos = [point.x, point.y, 0, 1] as vec4;
pos = [x, y, 0, 1] as vec4;
xyTransformMat4(pos, pos, matrix);
}
const w = pos[3];
Expand Down Expand Up @@ -218,7 +218,7 @@ function updateLineLabels(bucket: SymbolBucket,
// Awkward... but we're counting on the paired "vertical" symbol coming immediately after its horizontal counterpart
useVertical = false;

const anchorPos = project(new Point(symbol.anchorX, symbol.anchorY), posMatrix, getElevation);
const anchorPos = project(symbol.anchorX, symbol.anchorY, posMatrix, getElevation);

// Don't bother calculating the correct point for invisible labels.
if (!isVisible(anchorPos.point, clippingBuffer)) {
Expand Down Expand Up @@ -365,8 +365,8 @@ function placeGlyphsAlongLine(projectionContext: SymbolProjectionContext, symbol
if (!firstAndLastGlyph) {
return {notEnoughRoom: true};
}
const firstPoint = project(firstAndLastGlyph.first.point, glCoordMatrix, projectionContext.getElevation).point;
const lastPoint = project(firstAndLastGlyph.last.point, glCoordMatrix, projectionContext.getElevation).point;
const firstPoint = project(firstAndLastGlyph.first.point.x, firstAndLastGlyph.first.point.y, glCoordMatrix, projectionContext.getElevation).point;
const lastPoint = project(firstAndLastGlyph.last.point.x, firstAndLastGlyph.last.point.y, glCoordMatrix, projectionContext.getElevation).point;

if (keepUpright && !flip) {
const orientationChange = requiresOrientationChange(symbol.writingMode, firstPoint, lastPoint, aspectRatio);
Expand All @@ -386,10 +386,10 @@ function placeGlyphsAlongLine(projectionContext: SymbolProjectionContext, symbol
// Only a single glyph to place
// So, determine whether to flip based on projected angle of the line segment it's on
if (keepUpright && !flip) {
const a = project(projectionContext.tileAnchorPoint, posMatrix, projectionContext.getElevation).point;
const a = project(projectionContext.tileAnchorPoint.x, projectionContext.tileAnchorPoint.y, posMatrix, projectionContext.getElevation).point;
const tileVertexIndex = (symbol.lineStartIndex + symbol.segment + 1);
const tileSegmentEnd = new Point(projectionContext.lineVertexArray.getx(tileVertexIndex), projectionContext.lineVertexArray.gety(tileVertexIndex));
const projectedVertex = project(tileSegmentEnd, posMatrix, projectionContext.getElevation);
const projectedVertex = project(tileSegmentEnd.x, tileSegmentEnd.y, posMatrix, projectionContext.getElevation);
// We know the anchor will be in the viewport, but the end of the line segment may be
// behind the plane of the camera, in which case we can use a point at any arbitrary (closer)
// point on the segment.
Expand Down Expand Up @@ -461,7 +461,7 @@ function _projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint
// plane of the camera.
const unitVertexToBeProjected = previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit());
const projectedUnitVertex = projectionMatrix !== undefined ?
project(unitVertexToBeProjected, projectionMatrix, projectionContext.getElevation).point :
project(unitVertexToBeProjected.x, unitVertexToBeProjected.y, projectionMatrix, projectionContext.getElevation).point :
projectTileCoordinatesToViewport(unitVertexToBeProjected.x, unitVertexToBeProjected.y, projectionContext).point;
const projectedUnitSegment = previousProjectedPoint.sub(projectedUnitVertex);
return previousProjectedPoint.add(projectedUnitSegment._mult(minimumLength / projectedUnitSegment.mag()));
Expand Down Expand Up @@ -601,7 +601,7 @@ function projectTileCoordinatesToViewport(x: number, y: number, projectionContex
projection.point.x = (projection.point.x * 0.5 + 0.5) * projectionContext.width;
projection.point.y = (-projection.point.y * 0.5 + 0.5) * projectionContext.height;
} else {
projection = project(new Point(translatedX, translatedY), projectionContext.labelPlaneMatrix, projectionContext.getElevation);
projection = project(translatedX, translatedY, projectionContext.labelPlaneMatrix, projectionContext.getElevation);
projection.isOccluded = false;
}
return projection;
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 = 802382;
const expectedBytes = 803130;

expect(actualBytes - expectedBytes).toBeLessThan(increaseQuota);
expect(expectedBytes - actualBytes).toBeLessThan(decreaseQuota);
Expand Down

0 comments on commit c6a3d09

Please sign in to comment.