-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Improved transparency for line layers #11082
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
Conversation
@astojilj Yes I'm researching whether Something that slightly mitigates the aliasing is to reduce the alpha discard ratio until we reach a point where not too many pixels get affected by both stencil-out and alpha discard (here we can notice that it's a value somewhere around alpha test 1.0 |
This is really neat @karimnaaji! Looks cool and seems like it would solve a lot of pain points. const stencilIdPass2 = ~stencilIdPass1 & 255; I'm understanding this line correctly, the stencil is stripping out all pixels that are less than fully opaque. For more consistent appearances, would it make more sense to use a lower cutoff, maybe 50% opacity? The visual difference of diminished lines without antialising seems particularly noticeable in small shapes such as the yellow and orange lines in your second image. |
I think this is not useful here, but since I had it half-cobbled together and since someone mentioned my favorite extension (standard derivatives! 🎉 ), here's my standard derivative + lines antialiasing trick. The basic idea is that you pass a varying that's -1 on one side of the line and 1 on the other. If you divide that varying by the magnitude of the gradient ( https://observablehq.com/d/ca328ae703c13106 The image below shows triangulated lines without MSAA, instead using alpha for antialiasing. Note that the antialiasing width is the same whether on the part of the line close to the camera or whether far away. #extension GL_OES_standard_derivatives : enable
precision mediump float;
uniform float antialiasWidth, alpha;
varying float y;
void main () {
float gradient = length(vec2(dFdx(y), dFdy(y)));
gradient *= antialiasWidth;
float antialias = min(1.0, (1.0 - abs(y)) / gradient);
gl_FragColor = alpha * antialias * vec4(1, 0, 0, 1);
} |
@SnailBones Yes I agree that it's rather noticeable. I ended up splitting the cutoff in two different passes to fix the AA. Concerning In that specific case, I was trying to avoid falling back on a stencil identifier that was already used for tile stencil clipping, and stencil identifiers are allocated in ascending order, which means identifiers are very local: One thing that works is to have an identifier that falls far away from the current one, and also doesn't conflict with the tile clipping stenciling. So simply inverting it works nicely, so that stencil id Using that, we first draw once inverting it when the stencil test passes and any fragment falls through once. Then, in a second pass revert the stamp as we need to get it back to normal, so that it doesn't conflict with any other layer, but this time against the stencil id we used for clipping inverted ( @rreusser I love that technique but I found the derivatives difficult to leverage for the self-intersecting use case so I dropped that option for the moment and also found another way. I managed to fix the aliasing part by doing another pass and identifying which pixels contribute to AA, so the very edge of the polyline pixels are discarded in a first draw call with alpha discard threshold set to high, and then drawn once more with alpha discard threshold set to low: stencil tests (red fail/green pass) render And here's the original test that @astojilj mentioned, now fixed (some tweaks can be done to adjust alpha discard threshold and prevent a few pixels from being fully rejected in the middle parts): Refer https://103814-8629417-gh.circle-artifacts.com/0/test/integration/render-tests/index.html But this technique doesn't work with lines having 1 pixel width, but it doesn't matter as the artifact of self-intersecting geometry isn't noticeable anyway, we can simply disable it in that case. Failed test: The next step is to find a nice allocation strategy for the stencil identifiers, as this is not optimized since we should be able to save one draw call by simply leaving the stamp of the polyline footprint (as long as one transparent line layer stencil identifier isn't conflicting with another one). So we need to find a way to allocate stencil identifiers per line layer, while still not conflicting between different tile stencil clipping stencil ids and not reverting the stamp in an extra draw call. |
@SnailBones Concerning the next steps and to clarify a bit: Finding a better stencil allocation strategyAt the moment the stencil id used for the stamps is directly taken from the stencil clipping mask (refer here). And, as I previously mentioned, we flip it in order to have an id associated with the line stamp that does not conflict with stencil clipping (that cuts down geometry outside of the tile bounds). This has the advantage that it's relatively simple but, since we don't create a pair What I propose is to leave the stamp, but that means finding and creating a pair
One challenge and issue with that is that we're bound by the number of potential stencil id we have (0-255) and how many are already dedicated to tile clipping, when a collision happens we could just fallback to 3 draw calls (e.g. revert the stamp) and accept it as a limitation of the approach. Here's a more concrete case where this may happen: when drawing 100 tiles we can only draw ( Support with terrain stencil modeTerrain forks the stencil mode to have its own setup here. I haven't investigated in detail, but I think we should be able to do the same, we could probably create a Adding complex render test combination for different transparencyI would check whether we can use that sort of technique can be used with transparent It's something relatively used to create trail line for navigation trip representation. |
In the example image you shared, six tiles are visible onscreen with ids 0 through 5.1 If I understand the allocation strategy correctly, drawing a line on these tiles will use ids 255 down to 250. So then the above approach would create conflicts between the different line layers, and would need to be something like:
Or in the general case:
Here's an example of what IDs on the stencil buffer might look like with six tiles and two transparent line layers: By effectively giving every transparent line layer its own one-bit mask, bitmasking would also solve the problem of intersections. Here's the above example with this allocation strategy: The stencil buffer has only has 8 bits, so we'll still run out of space pretty quickly. Where Footnotes
|
d13be98
to
21c17f7
Compare
6999e5a
to
c72440e
Compare
678fbe8
to
e7f9143
Compare
The code looks great, such a thoughtful PR! 🙏 Did you try running the benchmark suite on the approach? I'm curious whether it impacts rendering performance and how much if so. |
Thanks for giving a look @mourner , I'll do benchmarking next. I expect the benchmark suite to run the same, as default styles don't overly make use of transparency, so this will need a bit more fine-grained profiling to be aware of the change. But as we may draw lines twice, we can expect render times to be higher for that case. Two optimizations have been added in this PR to mitigate that:
|
@mourner Fine-grained profiling (captured using gpu-time-layer) on this location with a style having using 4 transparent layers ( Higher-level metrics and profiling do not show a difference on our benchmark suite: I added a few render tests to finalize the PR. I think it's ready if we're ok with the tradeoff of render time mentioned above for improved quality. |
How common are translucent lines on typical styles like streets? 1.6x is a serious slowdown so let's make sure is doesn't affect most users. If it does, did we consider making an opt-in style-spec switch for this behavior? |
Verified and it's not common. I observed at most one layer, usually
In these styles transparent-lines are usually of more minimal use, or use lighter width to cover these artifacts, as they quickly show up otherwise. I've considered opt-in but it might not work well for these type of improvements, as it reduces discoverability and I really think we should try to give the best expected visual quality out of the box. But I'm open to more thoughts about it.
I'd also like to reiterate that point. Which is an optimization added in this PR that reduces the number of some draw calls by almost half on
I've extended the profiling to the GPU for the styles
Also refer to #11197, which is a cost that is now entirely removed as it was redundant:
|
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.
Great!
Co-authored-by: Karim Naaji <karim.naaji@gmail.com>
…gh `source cache`/`layer` switches Fix #11197
4b1c7b8
to
03a7c30
Compare
Can this optimization be merged into version 1.x ? |
@Amber-372 no. |
@mourner @karimnaaji is it possible to turn this “improvement” off? I had been using this for line-based heatmaps (brighter = more overlapping tracks), which this change breaks. Or do I need a couple of thousand layers to represent a couple of thousand tracks? This will still break tracks with multiple laps, which would only contribute one “level” of brightness to the map. |
@c-harding Yes it's possible. For context, consistent line opacity throughout the line is what most users expect by default for the majority of our use cases. It has been reported as a rendering artifact for a while before we introduced this change. Leveraging the opacity for line heatmap is less frequent, and something we could improve with a dedicated native layer to facilitate its use (which would allow to adjust the range of the possible values, how the opacity gets composited, and what color gradient to apply for that layer etc..). With that being said, we only enabled this change when used on |
Changelog:
<changelog>Improve quality of transparent line layers by removing overlapping geometry artifacts</changelog>
<changelog>Improve performance and reduce number of draw calls for high succession of source cache/layer switches</changelog>
A draft proof of concept to improve transparency of lines. When using line layers with opacity, overlapping geometry often results in poor transparency results, where one might expect a consistent color throughout the entire line, self-intersecting geometry often results in artifacts. Refer:
This PoC is an attempt at fixing that issue by utilizing the GPU instead of a geometric approach, as it is proven to be difficult to clean up self-intersecting polyline geometry (refer to @rreusser 's good idea from #10925).
Note that this PoC does not necessarily improve
line-blur
, but as noted in #4090, the most common complaints are around self-intersecting geometry that are mainly incoming from our line layers withline-opacity
. Since polylines can lead to rather complex self-intersections due to narrow angles and caps.The technique used here is to ultimately only invoke fragment operations once by stamping the stencil buffer with the polyline footprint before reverting the stamp to have the original buffer. By doing so, the self-intersecting geometry does not result in visual artifacts, since the fragment will fall-through only and exactly once.
Visualization of subsequent stencil tests:
The biggest tradeoff is that lines appear more aliased, since we need to use alpha-discard to remove fragments that passed a certain opacity threshold. There's no clear workaround at the moment for that issue since we're not rendering to texture. So one open question is whether this sort of improvement would be opt-in.
Overall, aliasing is a fair tradeoff for this technique, as it appears much less noticeable to the eye than the artifact resulting from overlapping transparent geometry (refer #4090 (comment), #4090 (comment), #4090 (comment)).
The technique doesn't only apply to lines and can be extended to fill layers.
A few references and after/before from our render tests to better understand the difference:
cc @rreusser