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

improve pitched legibility of labels that follow lines #4781

Merged
merged 8 commits into from
Jun 13, 2017

Conversation

ansis
Copy link
Contributor

@ansis ansis commented Jun 2, 2017

fixes #4718

This improves legibility of labels that follow lines in pitched views. The previous approach used the limited information in the shader to calculate put the glyph in approximatelyright place. The new approach does this more accurately by doing it on the cpu where we have access to the entire line geometry.

The core part of the pr is src/symbol/projection.js which does two things:

  • calculates the matrices used for projecting symbols
  • the find-the-correct-point-along-the-line-for-this-glyph calculations

See the comment in src/symbol/projection.js for an overview of the new approach.

This pr also gets rid of the the sliding-glyphs-for-each-segment-that-get-toggled-on-and-off approach, which simplifies things a lot.

This fixes:

  • garbled text near sharp corners
  • upside down labels caused by perspective sloping
  • glyph stretching and squishing
  • letter spacing with pitch-scaling

before / after

screen shot 2017-06-02 at 6 25 10 pm

screen shot 2017-06-02 at 6 23 44 pm

screen shot 2017-06-02 at 6 36 43 pm

screen shot 2017-06-02 at 6 37 00 pm

screen shot 2017-06-02 at 6 44 23 pm

screen shot 2017-06-02 at 6 43 25 pm

Benchmarks

Frame duration is about 1ms slower in benchmarks. In full page maps it's about 1-2 ms slower, and a bit more in pitched full page maps.

benchmark master 943dc39 along-line-label-legibility b5b6f9a
map-load 243 ms 138 ms
style-load 122 ms 119 ms
buffer 1,075 ms 1,069 ms
fps 60 fps 60 fps
frame-duration 4.2 ms, 0% > 16ms 5.2 ms, 0% > 16ms
query-point 1.55 ms 1.41 ms
query-box 73.66 ms 73.58 ms
geojson-setdata-small 12 ms 5 ms
geojson-setdata-large 137 ms 130 ms

Launch Checklist

  • briefly describe the changes in this PR
  • write tests for all new functionality
  • post benchmark scores
  • manually test the debug page

@ChrisLoer @kkaefer @anandthakker

@ansis
Copy link
Contributor Author

ansis commented Jun 2, 2017

and a couple wip shots:
screen shot 2017-05-12 at 11 17 33 am
screen shot 2017-05-12 at 11 45 20 am
screen shot 2017-05-12 at 12 50 14 pm

@ChrisLoer
Copy link
Contributor

This looks SOO good!

I won't have a chance to get into too much review until Monday, but to give you an idea for how the old max-camera-distance filter might still be relevant, look at:

14.27/34.004/-118.4845/-46.6/60 with viewport aligned road labels:

screenshot 2017-06-02 16 47 34


const zoom = this.zoom;
const placementZoom = Math.max(Math.log(scale) / Math.LN2 + zoom, 0);

const glyphOffsetArrayStart = this.glyphOffsetArray.length;

const labelAngle = Math.abs((labelAnchor.angle + placementAngle) % Math.PI);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment that labelAngle is over a 180 degree range relative to line orientation? I assumed 360 degrees at first and got confused reading the code.

if (anchor === 'viewport') {
const sinA = Math.sin(-this.transform.angle);
const cosA = Math.cos(-this.transform.angle);
const angle = inPixelUnits ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not immediately obvious here what inPixelUnits has to do with the anchor angle -- I had to go back and look at the use of getGlCoordMatrix in draw_symbol to puzzle it out. Maybe just an explanatory comment would help?

Copy link
Contributor

@ChrisLoer ChrisLoer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, looks beautiful, and fixes so many problems! My only nits were about commenting.

There is one significant architectural concern left, though: what do we do about collision boxes for line features? The pitch-scaling changes account for line labels getting bigger as they go into the distance, but they don't account for growth due to the effective increase in letter-spacing that this PR introduces. If you use a style with viewport-pitch-aligned road labels, it's pretty easy to get collisions in the distance for vertically oriented roads.

In updateLineLabels, if we deferred the addGlyph calls until we has processed the whole label, we could calculate something like an "expansion ratio" and pass that in as a fourth element of a_projected_pos. We could feed that into the collision_adjustment calculation, although it would have the same problem that the incidence_stretch logic has: it would grow the collision boxes in both x and y direction when we really just want them to expand in the x direction. We could try a similar solution: calculate an approximation of the shape at placement time, and at render time use the expansion factor to handle the difference. (Or: maybe just doing it at placement time is good enough)

*
* ## GL coordinate space
* At the end of everything, the vertex shader needs to produce a position in GL coordinate space,
* which is (-1, 1) at the top left and (1, -1) in the bottom left.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1, -1) should be bottom right?

* - map pixel space pitch-alignment=map rotation-alignment=map
* - rotated map pixel space pitch-alignment=map rotation-alignment=viewport
* - viewport pixel space pitch-alignment=viewport rotation-alignment=*
* 2. the the label follows a line, find the point along the line that is the correct distance from the anchor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the the label" -> "if the label"

const anchorPos = [symbol.anchorX, symbol.anchorY, 0, 1];
vec4.transformMat4(anchorPos, anchorPos, posMatrix);

// Don't bother calculating the correct point for invisible labels. Move them offscreen.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a note here that we have to move them offscreen instead of just dropping them because we're not updating the paired/non-dynamic buffers?

highp float distance_ratio = u_pitch_with_map ?
camera_to_anchor_distance / u_camera_to_center_distance :
u_camera_to_center_distance / camera_to_anchor_distance;
highp float perspective_ratio = 0.5 + 0.5 * clamp(distance_ratio, 0.1, 10.0);
Copy link
Contributor

@ChrisLoer ChrisLoer Jun 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, what case is the clamp addressing? Or is it just a way of saying "we expect a distance ratio of 10 to be effectively infinite"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was seeing things like this and thought it was an infinite/precision issue. Since I'm not seeing it now I think it might have been something else (broken inversion). Removing

float fontScale = u_is_text ? size / 24.0 : size;
vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1);
highp float camera_to_anchor_distance = projectedPoint.w;
highp float distance_ratio = u_pitch_with_map ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corresponding logic in projection.js is easier to understand, but here the inversion of distance_ratio is pretty hard to figure out since it's further removed from the rest of the projection logic. I would add a comment saying something like "If the label is pitched with the map, layout is done in pitched space, which makes labels in the distance smaller relative to viewport space. We counteract part of that effect by multiplying by the perspective ratio. If the label isn't pitched with the map, we do layout in viewport space, which makes labels in the distance larger relative to the features around them. We counteract part of that effect by dividing by the perspective ratio."

gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0);
}
highp float distance_ratio = u_pitch_with_map ?
camera_to_anchor_distance / u_camera_to_center_distance :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for symbol_icon

@ChrisLoer
Copy link
Contributor

Example of broken collision detection for vertically-oriented viewport-pitch-aligned road labels:

image

@ansis
Copy link
Contributor Author

ansis commented Jun 6, 2017

@ChrisLoer yeah, the collision boxes are a problem. I just tried adding an adjustment to maxScale to account for this and it seems like it mostly works: 6a789ec. Do you think this is good enough to last until we rewrite this?

Copy link
Contributor

@ChrisLoer ChrisLoer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hack looks surprisingly OK when I play around with it. But unless I'm missing something I think we need to apply rotation to the offsets before we calculate the adjustment factors? Kind of disconcerting that I can be pretty sure it's broken but the results still look OK...

@@ -75,7 +75,7 @@ class CollisionFeature {
// We calculate line collision boxes out to 150% of what would normally be our
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

150% -> 300% now, right? Because we've got n/2 padding boxes on either side and they're spaced out twice as far apart? Although in practice we often don't get that far because the line geometry doesn't let us.

const yStretch2 = yStretch;
const xSqr = box.offsetX * box.offsetX;
const ySqr = box.offsetY * box.offsetY;
const yStretchSqr = ySqr * yStretch2 * yStretch2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think yStretch2 adds much clarity here, it could just be yStretchSqr = ySqr * yStretch * yStretch.

// Adjust the max scale by (approximatePitchedLength / approximateRegularLength)
// to compensate for this.
const yStretch2 = yStretch;
const xSqr = box.offsetX * box.offsetX;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't these offsets need to have a rotation applied to them? I think as is this only works for labels that are relatively horizontal relative to tile orientation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, definitely

@ChrisLoer
Copy link
Contributor

This is a nit, and it could go in another PR, but maybe it would be better if we detected the orientation of a line label based on the beginning and the end of the label instead of the orientation of the middle line segment? The current approach can lead to labels that look "upside down" even though technically the segment their anchor is on is "right side up":

image

// If the current segment doesn't have enough remaining space, iterate forward along the line.
// Since all the glyphs are sorted by their distance from the anchor you never need to iterate backwards.
// This way line vertices are projected at most once.
while (offsetX >= segmentDistance + previousDistance && Math.abs(vertexIndex) < numVertices) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This while loop exits if we reach the end of the line (vertexIndex >= numVertices), but we keep adding glyphs onto whatever the last segment was. This leads to some funky behavior:

  • Ignoring collisions (because collision boxes only go as long as the line)
  • Road labels extending past the end of the road
  • Road labels wrapping back on themselves (see Ocean Park Blvd in the screen capture)... although this one might be better addressed by a max-curvature property.

turnabout is fair play

We should hide the label instead if we hit this limit.

@ChrisLoer
Copy link
Contributor

With an eye on mapbox/mapbox-gl-native#9009 (comment), we should try to see if we can stay under the 8 vertex attribute limit when we make these changes on the JS side.

since we should move towards treating a_size as a regular data-driven property we might not want to pack into it.

Do we have a choice? On this PR a_data is using 40 bits and a_size is using 32 bits, and I believe the only extra space is 16 bits in a_projected_pos. If we keep a_size and try to get rid of a_data, we'd have 40 - 16 -> 24 bits to pack in somewhere else. We can drop a few bits of precision from the offset in a_pos_offset (dropping from 1/64 precision to 1/8 saves 6 bits and doesn't look too noticeably different although the render tests do change), and a few bits from the anchor position. You could also go back to dropping 8 bits of precision from segment_angle (but that'd be a bummer, because the low precision angles meant that labels sometimes didn't quite line up with their lines).

@ansis ansis force-pushed the along-line-label-legibility branch 4 times, most recently from ac0998d to d2ea30b Compare June 12, 2017 16:04
@ansis
Copy link
Contributor Author

ansis commented Jun 12, 2017

@ChrisLoer I fixed the collision box rotation bug and packed all the symbol vertex data into 8 attributes. I think flipping the label based on start and endpoints instead of midpoints can be handled in another pr.

Copy link
Contributor

@ChrisLoer ChrisLoer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

I filed #4825 for the text orientation issue, and #4826 for the "labels can flip back on themselves issue".

Out of curiosity, why did you pack the label angle into the third part of a_projected_pos instead of just adding a fourth item to the vector? Do we think there's a performance benefit? I was a little sad to see the increased label angle precision go away (and a little surprised that text-writing-mode/chinese didn't have to be reverted after the angle change)

@ansis ansis force-pushed the along-line-label-legibility branch from 75f0765 to 28d553f Compare June 12, 2017 20:39
@ansis
Copy link
Contributor Author

ansis commented Jun 12, 2017

I just rebased on master which includes the recent *-offset fix (#4779). The new implementation is here for alongLine labels and here for horizontal labels.

Out of curiosity, why did you pack the label angle into the third part of a_projected_pos instead of just adding a fourth item to the vector? Do we think there's a performance benefit? I was a little sad to see the increased label angle precision go away (and a little surprised that text-writing-mode/chinese didn't have to be reverted after the angle change)

I'm not sure, I guess the impulse was to just pack everything as tightly as possible instead of using an extra 4 bytes. Should I switch to using a fourth item?

my remaining items:

  • fix the x component of text-offset for labels that follow lines
  • fix failing query test

@ansis
Copy link
Contributor Author

ansis commented Jun 12, 2017

@ChrisLoer I also made a query test less sensitive to avoid a failure. The test shouldn't be less sensitive but I don't think we can fix this properly before working on all the collision stuff.

@ansis ansis force-pushed the along-line-label-legibility branch from 28d553f to ef07398 Compare June 12, 2017 21:11
The correct position of a glyph along is now found by doing the work on
the CPU where we have access to the entire line geometry.

For performance, as much work as possible is left to be done in the
shaders. This includes all the work for point horizontal labels.

ref #4718
Apply pitch scaling to the font size before calculating the glyph's
position.
to fit within the minimum number of supported vertex attributes
This also changes how we iterate along the line to find the correct
point. The previous algorithm sorted the quads by offset (during bucket
creation) and then iterated along the line a single time, continuing
from the previous point.

The new approach is simpler. It iterates for each glyph independently
and doesn't require them to be sorted. It's a tiny bit slower but it's
also easier to work with. For example, implementing text-offset changes
the sorted order you'd need for the previous algorithm.

As we explore other improvements like #4825 #4826 this will be easier to
work with. We can always optimize again later when things settle.
@ansis ansis force-pushed the along-line-label-legibility branch from ef07398 to ce09894 Compare June 13, 2017 16:48
@ansis ansis merged commit ce09894 into master Jun 13, 2017
@jfirebaugh jfirebaugh deleted the along-line-label-legibility branch June 14, 2017 21:28
ansis added a commit to mapbox/mapbox-gl-native that referenced this pull request Jun 30, 2017
port mapbox/mapbox-gl-js#4781

This improves legibility of labels that follow lines in pitched views.
The previous approach used the limited information in the shader to
calculate put the glyph in approximatelyright place. The new approach
does this more accurately by doing it on the cpu where we have access to
the entire line geometry.
ansis added a commit to mapbox/mapbox-gl-native that referenced this pull request Jul 6, 2017
port mapbox/mapbox-gl-js#4781

This improves legibility of labels that follow lines in pitched views.
The previous approach used the limited information in the shader to
calculate put the glyph in approximatelyright place. The new approach
does this more accurately by doing it on the cpu where we have access to
the entire line geometry.
ChrisLoer pushed a commit to mapbox/mapbox-gl-native that referenced this pull request Jul 7, 2017
port mapbox/mapbox-gl-js#4781

This improves legibility of labels that follow lines in pitched views.
The previous approach used the limited information in the shader to
calculate put the glyph in approximatelyright place. The new approach
does this more accurately by doing it on the cpu where we have access to
the entire line geometry.
ansis added a commit to mapbox/mapbox-gl-native that referenced this pull request Jul 10, 2017
port mapbox/mapbox-gl-js#4781

This improves legibility of labels that follow lines in pitched views.
The previous approach used the limited information in the shader to
calculate put the glyph in approximatelyright place. The new approach
does this more accurately by doing it on the cpu where we have access to
the entire line geometry.
ansis added a commit to mapbox/mapbox-gl-native that referenced this pull request Jul 11, 2017
port mapbox/mapbox-gl-js#4781

This improves legibility of labels that follow lines in pitched views.
The previous approach used the limited information in the shader to
calculate put the glyph in approximatelyright place. The new approach
does this more accurately by doing it on the cpu where we have access to
the entire line geometry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

transform symbol vertices outside of shaders
2 participants