Skip to content

Commit

Permalink
eliminate redundant allocations in symbol projection
Browse files Browse the repository at this point in the history
  • Loading branch information
mourner committed Nov 7, 2022
1 parent 9a26470 commit 59e7d1f
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 61 deletions.
6 changes: 3 additions & 3 deletions src/render/draw_symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var
];

const projectedAnchor = symbolProjection.projectVector(reprojectedAnchor, pitchWithMap ? tileMatrix : labelPlaneMatrix);
const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.getCameraToCenterDistance(bucket.getProjection()), projectedAnchor.signedDistanceFromCamera);
const perspectiveRatio = symbolProjection.getPerspectiveRatio(transform.getCameraToCenterDistance(bucket.getProjection()), projectedAnchor[3]);
let renderTextSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, size, symbol) * perspectiveRatio / ONE_EM;
if (pitchWithMap) {
// Go from size in pixels to equivalent size in tile units
Expand All @@ -221,10 +221,10 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var
z + anchorElevation * upDir[2] * upVectorScale.metersToTile
];

shiftedAnchor = symbolProjection.projectVector(reprojectedShiftedAnchor, labelPlaneMatrix).point;
shiftedAnchor = symbolProjection.projectVector(reprojectedShiftedAnchor, labelPlaneMatrix);
} else {
const rotatedShift = rotateWithMap ? shift.rotate(-transform.angle) : shift;
shiftedAnchor = [projectedAnchor.point[0] + rotatedShift.x, projectedAnchor.point[1] + rotatedShift.y, 0];
shiftedAnchor = [projectedAnchor[0] + rotatedShift.x, projectedAnchor[1] + rotatedShift.y, 0];
}

const angle = (bucket.allowVerticalPlacement && symbol.placedOrientation === WritingMode.vertical) ? Math.PI / 2 : 0;
Expand Down
45 changes: 21 additions & 24 deletions src/symbol/collision_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class CollisionIndex {
const {perspectiveRatio} = screenAnchorPoint;
const labelPlaneFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;
const labelPlaneFontScale = labelPlaneFontSize / ONE_EM;
const labelPlaneAnchorPoint = projection.project(new Point(elevatedAnchor[0], elevatedAnchor[1]), labelPlaneMatrix, elevatedAnchor[2]).point;
const labelPlaneAnchorPoint = projection.project(elevatedAnchor[0], elevatedAnchor[1], labelPlaneMatrix, elevatedAnchor[2]);

const projectionCache = {};
const lineOffsetX = symbol.lineOffsetX * labelPlaneFontScale;
Expand Down Expand Up @@ -224,52 +224,49 @@ class CollisionIndex {
// The path might need to be converted into screen space if a pitched map is used as the label space
if (labelToScreenMatrix) {
assert(pitchWithMap);
const screenSpacePath = (elevation && !isGlobe) ?
projectedPath = (elevation && !isGlobe) ?
projectedPath.map((p, index) => {
const elevation = getElevation(index < first.path.length - 1 ? first.tilePath[first.path.length - 1 - index] : last.tilePath[index - first.path.length + 2]);
p[2] = elevation[2];
return projection.projectVector((p: any), labelToScreenMatrix);
return projection.projectVector(p, labelToScreenMatrix);
}) :
projectedPath.map(p => projection.projectVector((p: any), labelToScreenMatrix));
projectedPath.map(p => projection.projectVector(p, labelToScreenMatrix));

// Do not try to place collision circles if even of the points is behind the camera.
// This is a plausible scenario with big camera pitch angles
if (screenSpacePath.some(point => point.signedDistanceFromCamera <= 0)) {
if (projectedPath.some(point => point[3] <= 0)) {
projectedPath = [];
} else {
projectedPath = screenSpacePath.map(p => p.point);
}
}

let segments = [];

if (projectedPath.length > 0) {
const screenSpacePath = projectedPath.map(p => new Point(p[0], p[1]));

// Quickly check if the path is fully inside or outside of the padded collision region.
// For overlapping paths we'll only create collision circles for the visible segments
let minx = Infinity;
let maxx = -Infinity;
let miny = Infinity;
let maxy = -Infinity;

for (let i = 0; i < screenSpacePath.length; i++) {
minx = Math.min(minx, screenSpacePath[i].x);
miny = Math.min(miny, screenSpacePath[i].y);
maxx = Math.max(maxx, screenSpacePath[i].x);
maxy = Math.max(maxy, screenSpacePath[i].y);
for (const p of projectedPath) {
minx = Math.min(minx, p[0]);
miny = Math.min(miny, p[1]);
maxx = Math.max(maxx, p[0]);
maxy = Math.max(maxy, p[1]);
}

if (minx >= screenPlaneMin.x && maxx <= screenPlaneMax.x &&
miny >= screenPlaneMin.y && maxy <= screenPlaneMax.y) {
// Quad fully visible
segments = [screenSpacePath];
} else if (maxx < screenPlaneMin.x || minx > screenPlaneMax.x ||
maxy < screenPlaneMin.y || miny > screenPlaneMax.y) {
// Not visible
segments = [];
} else {
segments = clipLine([screenSpacePath], screenPlaneMin.x, screenPlaneMin.y, screenPlaneMax.x, screenPlaneMax.y);
// Path visible
if (maxx >= screenPlaneMin.x && minx <= screenPlaneMax.x &&
maxy >= screenPlaneMin.y && miny <= screenPlaneMax.y) {

segments = [projectedPath.map(p => new Point(p[0], p[1]))];

if (minx < screenPlaneMin.x || maxx > screenPlaneMax.x ||
miny < screenPlaneMin.y || maxy > screenPlaneMax.y) {
// Path partially visible, clip
segments = clipLine(segments, screenPlaneMin.x, screenPlaneMin.y, screenPlaneMax.x, screenPlaneMax.y);
}
}
}

Expand Down
66 changes: 32 additions & 34 deletions src/symbol/projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ import {CanonicalTileID, OverscaledTileID} from '../source/tile_id.js';
import {calculateGlobeLabelMatrix} from '../geo/projection/globe_util.js';
export {updateLineLabels, hideGlyphs, getLabelPlaneMatrixForRendering, getLabelPlaneMatrixForPlacement, getGlCoordMatrix, project, projectVector, projectClamped, getPerspectiveRatio, placeFirstAndLastGlyph, placeGlyphAlongLine, xyTransformMat4};

type ProjectedSymbol = {|
point: Vec3,
signedDistanceFromCamera: number
|};
type PlacedGlyph = {|
angle: number,
path: Array<Vec3>,
Expand Down Expand Up @@ -183,39 +179,41 @@ function getGlCoordMatrix(posMatrix: Float32Array,
}
}

function project(point: {x: number, y: number}, matrix: Mat4, elevation: number = 0): ProjectedSymbol {
const pos = [point.x, point.y, elevation, 1];
function project(x: number, y: number, matrix: Mat4, elevation: number = 0): Vec4 {
const pos = [x, y, elevation, 1];
if (elevation) {
vec4.transformMat4(pos, pos, matrix);
} else {
xyTransformMat4(pos, pos, matrix);
}
const w = pos[3];
return {
point: [pos[0] / w, pos[1] / w, pos[2] / w],
signedDistanceFromCamera: w
};
pos[0] /= w;
pos[1] /= w;
pos[2] /= w;
return pos;
}

function projectVector(point: [number, number, number], matrix: Mat4): ProjectedSymbol {
function projectVector(point: Vec3, matrix: Mat4): Vec4 {
const pos = [point[0], point[1], point[2], 1];
vec4.transformMat4(pos, pos, matrix);
const w = pos[3];
return {
point: [pos[0] / w, pos[1] / w, pos[2] / w],
signedDistanceFromCamera: w
};
pos[0] /= w;
pos[1] /= w;
pos[2] /= w;
return pos;
}

function projectClamped(point: [number, number, number], matrix: Mat4): Vec4 {
function projectClamped(point: Vec3, matrix: Mat4): Vec4 {
const pos = [point[0], point[1], point[2], 1];
vec4.transformMat4(pos, pos, matrix);

// Clamp distance to a positive value so we can avoid screen coordinate
// being flipped possibly due to perspective projection
const w = Math.max(pos[3], 0.000001);

return [pos[0] / w, pos[1] / w, pos[2] / w, w];
const w = pos[3] = Math.max(pos[3], 0.000001);
pos[0] /= w;
pos[1] /= w;
pos[2] /= w;
return pos;
}

function getPerspectiveRatio(cameraToCenterDistance: number, signedDistanceFromCamera: number): number {
Expand Down Expand Up @@ -317,10 +315,10 @@ function updateLineLabels(bucket: SymbolBucket,
const fontSize = symbolSize.evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol);
const pitchScaledFontSize = pitchWithMap ? fontSize / perspectiveRatio : fontSize * perspectiveRatio;

const labelPlaneAnchorPoint = project(new Point(elevatedAnchor[0], elevatedAnchor[1]), labelPlaneMatrix, elevatedAnchor[2]);
const labelPlaneAnchorPoint = project(elevatedAnchor[0], elevatedAnchor[1], labelPlaneMatrix, elevatedAnchor[2]);

// Skip labels behind the camera
if (labelPlaneAnchorPoint.signedDistanceFromCamera <= 0.0) {
if (labelPlaneAnchorPoint[3] <= 0.0) {
hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray);
continue;
}
Expand All @@ -329,15 +327,15 @@ function updateLineLabels(bucket: SymbolBucket,

const getElevationForPlacement = pitchWithMap ? null : getElevation; // When pitchWithMap, we're projecting to scaled tile coordinate space: there is no need to get elevation as it doesn't affect projection.
const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint.point, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap);
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap);

useVertical = placeUnflipped.useVertical;

if (getElevationForPlacement && placeUnflipped.needsFlipping) projectionCache = {}; // Truncated points should be recalculated.
if (placeUnflipped.notEnoughRoom || useVertical ||
(placeUnflipped.needsFlipping &&
placeGlyphsAlongLine(symbol, pitchScaledFontSize, true /*flipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix,
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint.point, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap).notEnoughRoom)) {
bucket.glyphOffsetArray, lineVertexArray, dynamicLayoutVertexArray, globeExtVertexArray, labelPlaneAnchorPoint, tileAnchorPoint, projectionCache, aspectRatio, getElevationForPlacement, tr.projection, tileID, pitchWithMap).notEnoughRoom)) {
hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray);
}
}
Expand Down Expand Up @@ -449,8 +447,8 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
if (!firstAndLastGlyph) {
return {notEnoughRoom: true};
}
const firstVec = projectVector((firstAndLastGlyph.first.point: any), glCoordMatrix).point;
const lastVec = projectVector((firstAndLastGlyph.last.point: any), glCoordMatrix).point;
const firstVec = projectVector(firstAndLastGlyph.first.point, glCoordMatrix);
const lastVec = projectVector(firstAndLastGlyph.last.point, glCoordMatrix);

const firstPoint = new Point(firstVec[0], firstVec[1]);
const lastPoint = new Point(lastVec[0], lastVec[1]);
Expand All @@ -475,16 +473,16 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
// 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(tileAnchorPoint, posMatrix).point;
const a = project(tileAnchorPoint.x, tileAnchorPoint.y, posMatrix);
const tileVertexIndex = (symbol.lineStartIndex + symbol.segment + 1);
// $FlowFixMe
const tileSegmentEnd = new Point(lineVertexArray.getx(tileVertexIndex), lineVertexArray.gety(tileVertexIndex));
const projectedVertex = project(tileSegmentEnd, posMatrix);
const projectedVertex = project(tileSegmentEnd.x, tileSegmentEnd.y, posMatrix);
// 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.
const b = (projectedVertex.signedDistanceFromCamera > 0) ?
projectedVertex.point :
const b = (projectedVertex[3] > 0) ?
projectedVertex :
projectTruncatedLineSegment(tileAnchorPoint, tileSegmentEnd, a, 1, posMatrix, undefined, projection, tileID.canonical);

const orientationChange = requiresOrientationChange(symbol, new Point(a[0], a[1]), new Point(b[0], b[1]), aspectRatio);
Expand Down Expand Up @@ -521,11 +519,11 @@ function placeGlyphsAlongLine(symbol, fontSize, flip, keepUpright, posMatrix, la
function elevatePointAndProject(p: Point, tileID: CanonicalTileID, posMatrix: Float32Array, projection: Projection, getElevation: ?((p: Point) => Array<number>)) {
const point = projection.projectTilePoint(p.x, p.y, tileID);
if (!getElevation) {
return project(point, posMatrix, point.z);
return project(point.x, point.y, posMatrix, point.z);
}

const elevation = getElevation(p);
return project(new Point(point.x + elevation[0], point.y + elevation[1]), posMatrix, point.z + elevation[2]);
return project(point.x + elevation[0], point.y + elevation[1], posMatrix, point.z + elevation[2]);
}

function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint: Point, previousProjectedPoint: Vec3, minimumLength: number, projectionMatrix: Float32Array, getElevation: ?((p: Point) => Array<number>), projection: Projection, tileID: CanonicalTileID): Vec3 {
Expand All @@ -534,7 +532,7 @@ function projectTruncatedLineSegment(previousTilePoint: Point, currentTilePoint:
// point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the
// plane of the camera.
const unitVertex = previousTilePoint.add(previousTilePoint.sub(currentTilePoint)._unit());
const projectedUnitVertex = elevatePointAndProject(unitVertex, tileID, projectionMatrix, projection, getElevation).point;
const projectedUnitVertex = elevatePointAndProject(unitVertex, tileID, projectionMatrix, projection, getElevation);
const projectedUnitSegment = vec3.sub([], previousProjectedPoint, projectedUnitVertex);

return vec3.scaleAndAdd([], previousProjectedPoint, projectedUnitSegment, minimumLength / vec3.length(projectedUnitSegment));
Expand Down Expand Up @@ -616,8 +614,8 @@ function placeGlyphAlongLine(
if (current === undefined) {
currentVertex = new Point(lineVertexArray.getx(currentIndex), lineVertexArray.gety(currentIndex));
const projection = elevatePointAndProject(currentVertex, tileID.canonical, labelPlaneMatrix, reprojection, getElevation);
if (projection.signedDistanceFromCamera > 0) {
current = projectionCache[currentIndex] = projection.point;
if (projection[3] > 0) {
current = projectionCache[currentIndex] = projection;
} else {
// The vertex is behind the plane of the camera, so we can't project it
// Instead, we'll create a vertex along the line that's far enough to include the glyph
Expand Down

0 comments on commit 59e7d1f

Please sign in to comment.