-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Conversation
src/data/bucket/symbol_bucket.js
Outdated
|
||
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); |
There was a problem hiding this comment.
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.
src/render/painter.js
Outdated
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.
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?
There was a problem hiding this 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)
src/symbol/projection.js
Outdated
* | ||
* ## 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.
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?
src/symbol/projection.js
Outdated
* - 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.
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"
src/symbol/projection.js
Outdated
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.
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?
src/shaders/symbol_icon.vertex.glsl
Outdated
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.
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"?
There was a problem hiding this comment.
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
src/shaders/symbol_icon.vertex.glsl
Outdated
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.
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."
src/shaders/symbol_sdf.vertex.glsl
Outdated
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.
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 yeah, the collision boxes are a problem. I just tried adding an adjustment to |
There was a problem hiding this 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...
src/symbol/collision_feature.js
Outdated
@@ -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.
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.
src/symbol/collision_tile.js
Outdated
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.
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
.
src/symbol/collision_tile.js
Outdated
// 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.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, definitely
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": |
src/symbol/projection.js
Outdated
// 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.
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.
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
There was a problem hiding this 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)
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.js
which does two things: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:
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