Repository navigation
improve pitched legibility of labels that follow lines - #4781
Conversation
|
|
||
| const glyphOffsetArrayStart = this.glyphOffsetArray.length; | ||
|
|
||
| const labelAngle = Math.abs((labelAnchor.angle + placementAngle) % Math.PI); |
There was a problem hiding this comment.
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 ? |
There was a problem hiding this comment.
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?
ChrisLoer
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
(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. |
There was a problem hiding this comment.
"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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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 ? |
There was a problem hiding this comment.
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 : |
There was a problem hiding this comment.
Same comment as for symbol_icon
|
@ChrisLoer yeah, the collision boxes are a problem. I just tried adding an adjustment to |
ChrisLoer
left a comment
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
|
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": |
| // 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) { |
There was a problem hiding this comment.
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-curvatureproperty.
We should hide the label instead if we hit this limit.
|
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.
Do we have a choice? On this PR |
ac0998d to
d2ea30b
Compare
|
@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. |
d2ea30b to
75f0765
Compare
ChrisLoer
left a comment
There was a problem hiding this comment.
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)
75f0765 to
28d553f
Compare
|
I just rebased on master which includes the recent
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:
|
|
@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. |
28d553f to
ef07398
Compare
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.
ef07398 to
ce09894
Compare
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.
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.
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.
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.
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.







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.jswhich does two things:See the comment in
src/symbol/projection.jsfor 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:
before / after
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.
Launch Checklist
@ChrisLoer @kkaefer @anandthakker