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

Line features in GL don't render correctly #596

Closed
manthey opened this issue Jul 11, 2016 · 12 comments
Closed

Line features in GL don't render correctly #596

manthey opened this issue Jul 11, 2016 · 12 comments
Assignees
Labels

Comments

@manthey
Copy link
Contributor

manthey commented Jul 11, 2016

Wide lines at acute vertices appear to have the most trouble:

image

@manthey manthey changed the title Line feature in GL don't render correctly Line features in GL don't render correctly Jul 18, 2016
@manthey
Copy link
Contributor Author

manthey commented Jul 18, 2016

There are three separate bugs here.

One is that some angles were miscalculated. A fix for that is part of PR #597.

The second is the inside of acute angles. If a line segment is short enough, the interior vertex of the intersection is badly placed.

The third is when a miter is too long, we truncate it in a manner that often collapses the line width.

@manthey
Copy link
Contributor Author

manthey commented Jul 19, 2016

Here is an image that shows a variety of line artifacts. The blue lines are full-opacity lines with repeated junction points, while the grey lines are half-opacity lines without repeated junctions (i.e., if the grey line is [A, B, C, D], the blue line is [A, B, B, C, C, D]).

From left to right:

  • Wide lines that overlap do not have constant opacity. I don't see a way to fix this short of computing the bounding polygon for the line with width and then rendering it as a generic polygon. We probably will just accept this as a limitation.
  • Lines that exceed the miter limit don't render correctly. We need more vertices to properly render a bevel join. If we switch to generating the join normals during line vertex creation (as opposed to computing them in the vertex shader), we could generate an intermediate line segment or triangle when we exceed the miter limit. Otherwise, we should at least detect this in the line vertex creation and pass a repeated vertex for the next or previous vertex.
  • The center polyline is drawn correctly.
  • This shows again the wide line overlap opacity issue.
  • Lines that are wide, short, and joined at acute angles generate strange triangles.

image

@manthey
Copy link
Contributor Author

manthey commented Nov 14, 2016

In addressing this issue, we have the opportunity to also features to our lines.

Features we can add:

  • Line cap style (butt, round, square)
  • Line join style (miter, bevel, round, miter-clip)
  • Miter limit (currently this isn't configurable)
  • Stroke offset. This could be simple, such as drawing the entire line to the center, left, or right, or it could be a float value that offsets the line anywhere between those locations.
  • Antialiasing (the can be configurable in its magnitude).

Features that aren't being considered right now:

  • Dashing
  • Textured lines (these would be straightforward to add in the 2-D case).

Currently, we draw each line as a pair of triangles. When we process the vertices in the vertex shader, we use three points and an offset for a total of 10 float values, plus the color (3 values), opacity, and line width, for a total of 15 float values per vertex. This means that on graphics card that allows 1Gb of memory for attributes that we can draw 17.8 million line vertices, of 8.9 million line segments that aren't joined to others.

There are three technical approaches to fixing the errors we have and adding the features in the top list:

  • (A) Make the geometry more involved that 2 triangles per segment. This requires a fair amount of preprocessing, but has the virtue that the shaders are very simple. It could be troublesome to make a general case that handles 3-D information efficiently. If webGL 1.x supported geometry shaders, that could be used instead of preprocessing.
  • (B) Preprocess information prior to generating our data to optimize for 2D lines. Instead of including the next and previous vertices and computing angles in the vertex shader, compute angles in the preprocessing. This makes extending to 3-D difficult, but reduces the time spend in the vertex shader and reduces the memory requirements from 15 to 11 floats per vertex.
  • (C) Pass more information to the vertex shader so that the two triangles contain all necessary information for the various join conditions. This has the virtue that 3D and perspective transforms can be done within the shaders, but will increase the memory from 15 to 18 floats per vertex. It also makes the vertex shader more complex (but the preprocessing less so).

In thinking of how lines are requested, there are many varied conditions. For instance, if a series of connected line segments have repeated vertices, (a - b - b - c - d - d - d - e), I think this should look identical to the same series of segments without repeated vertices (a - b - c - d - e). Currently, if we have one repeated vertex, we handle it as if it wasn't repeated, but if we have multiple repeated vertices, we do odd things, such as treat them as a line segment of zero length that is oriented horizontally on the screen for purposes of mitering. This applies to functionally repeated vertices (one that have the same x and y in the screen coordinates).

I'm currently considering option (B) to be the best compromise to handle repeated vertices, use less memory, and allow all of the options we care about.

Thoughts and opinions welcome.

@jeffbaumes
Copy link
Contributor

Have you looked at this library?

https://github.com/mattdesl/extrude-polyline

@jbeezley
Copy link
Contributor

Also worth considering is how mapbox-gl does it because they already do dashing, textured lines, 3D, and antialiasing efficiently enough to support vector tiling. For reference, the preprocessing is done here and the shader code is in a different repo here.

@aashish24
Copy link
Member

Option B is fine and I believe that's what mapbox does, however, the parallel processing of vertex shader might be faster in certain cases. The limitation there would be is that you won't get any additional vertices in WebGL 1.x so in that sense B is the way to go. It would be interesting to see the timing differences just with that change if possible. I think in general B will provide most flexibility for the options we talked about with different joints. I have looked at the mapbox gl code before and I think it would be worth picking some ideas from it.

@jbeezley
Copy link
Contributor

Also worth noting is that mapbox performs much (if not all) preprocessing in WebWorkers to achieve the performance necessary for tiling. I suspect we will hit bottlenecks with this unless we go down a similar route.

@aashish24
Copy link
Member

https://github.com/mattdesl/extrude-polyline

This library looks interesting but seems to have inconsistent stroke widths on the corners; also the overlapping lines seems bit odd but it would be worth looking into it since it is using GL as well.

@aashish24
Copy link
Member

Also worth noting is that mapbox performs much (if not all) preprocessing in WebWorkers to achieve the performance necessary for tiling. I suspect we will hit bottlenecks with this unless we go down a similar route.

+1, performance was my concern with too much work on JS side of things with webworkers. Should we look into using webworkes? Or would it be a post 1.0 thing?

@jbeezley
Copy link
Contributor

Making everything compatible with WebWorkers would take quite a lot of refactoring and make a significant departure from the current API. If it went in after 1.0, it would most likely warrant a 2.0 release.

@aashish24
Copy link
Member

Making everything compatible with WebWorkers would take quite a lot of refactoring and make a significant departure from the current API. If it went in after 1.0, it would most likely warrant a 2.0 release.

+1, I agree

@manthey I guess in that case you may want to try out both B) and C) and see what works better in our current system. I would think B will provide most flexibility but may cost us performance and C) will probably give better performance but at the cost of more memory. I think memory would be less concern for me right now.

manthey added a commit that referenced this issue Nov 21, 2016
This resolves issues #204 and #596.

Previously, lines were not rendered correctly for several conditions: (a) miter locations were sometimes miscalculated, resulting in inconsistent line widths.  (b) when the miter limit was exceeded, an attempt to prevevnt an excessively long line produced skewed segments.  (c) On the inside of acute angles on short line segments, some of the triangles used to render lines were twisted.  (d) when lines overlap, the overlap was inconsistent.

All of this has been fixed.  Additionally, we know support:
- line caps: butt (default and what was avaulable before), round, square.  These can vary by point.
- line joins: miter (default), round, bevel, miter-clip.  These can vary by point.  Miter and miter-clip have a configurable miter limit (one value for the whol feature) which, if the miter would be longer than this, switches to using a bevel or a clipped miter.
- antialiasing on edges and end caps.  This can be specified for the whole feature.
- strokeOffset: This can vary from -1 (shift left) to 1 (shift right), where 0 is the line centered on the vertices, and 1 is with the edge of the line on the vertices.

All of this is done by rendering two triangles per line segment and performing much of the computation in the vertex and fragment shaders.

There are a few limitations to the current implementation:
- there is finite precision in the calculations, resulting in occasional artifacts along miter joins.  These are mostly only noticeable on very wide lines using stroke offsets, and, even then, are subtle.
- When the line width changes per vertex, sometimes the inside joins are not technically correct.  Instead of using the angle of the lines meeting, the angle of the edges of the lines meeting would need to be calculated.
- On short line segments, one edge of the miter can be by itself, which would ideally either be antialiased or wouldn't be trimmed.  Currently, on wide short lines with multiple segments, the mitered edge can look blocky in limited instances.

This method uses 18/15 as much memory as lines used before.

There is a debug flag to show all of the pixels sent to the fragment shader.  If debug is not specified when the feature is created, the debug code is not even compiled into the fragment shader.

There could be some efficiency improvements in the shaders.  For instance, it might worth it to have a quick test for simple line segments without joins.

The new features and changes are exposed in one of the selenium tests (test/selenium/glLines/?wide=true), which can take a variety of query options, such as strokeOffset, lineCap, lineJoin, miterLimit, antialiasing, strokeWidth, strokeColor, strokeOpacity, and debug.  This should be turned into an example that also tests number of lines.
manthey added a commit that referenced this issue Nov 21, 2016
This resolves issues #204 and #596.

Previously, lines were not rendered correctly for several conditions: (a) miter locations were sometimes miscalculated, resulting in inconsistent line widths.  (b) when the miter limit was exceeded, an attempt to prevent an excessively long line produced skewed segments.  (c) On the inside of acute angles on short line segments, some of the triangles used to render lines were twisted.  (d) when lines overlap, the overlap was inconsistent.

All of this has been fixed.  Additionally, we now support:
- line caps: butt (default and what was avaulable before), round, square.  These can vary by point.
- line joins: miter (default), round, bevel, miter-clip.  These can vary by point.  Miter and miter-clip have a configurable miter limit (one value for the whole feature) which, if the miter would be longer than this, switches to using a bevel or a clipped miter.
- antialiasing on edges and end caps.  This can be specified for the whole feature.
- strokeOffset: This can vary from -1 (shift left) to 1 (shift right), where 0 is the line centered on the vertices, and 1 is with the edge of the line on the vertices.

All of this is done by rendering two triangles per line segment and performing much of the computation in the vertex and fragment shaders.

There are a few limitations to the current implementation:
- there is finite precision in the calculations, resulting in occasional artifacts along miter joins.  These are mostly only noticeable on very wide lines using stroke offsets, and, even then, are subtle.
- When the line width changes per vertex, sometimes the inside joins are not technically correct.  Instead of using the angle of the lines meeting, the angle of the edges of the lines meeting would need to be calculated.
- On short line segments, one edge of the miter can be by itself, which would ideally either be antialiased or wouldn't be trimmed.  Currently, on wide short lines with multiple segments, the mitered edge can look blocky in limited instances.

This method uses 18/15 as much memory as lines used before.

There is a debug flag to show all of the pixels sent to the fragment shader.  If debug is not specified when the feature is created, the debug code is not even compiled into the fragment shader.

There could be some efficiency improvements in the shaders.  For instance, it might worth it to have a quick test for simple line segments without joins.

The new features and changes are exposed in one of the selenium tests (test/selenium/glLines/?wide=true), which can take a variety of query options, such as strokeOffset, lineCap, lineJoin, miterLimit, antialiasing, strokeWidth, strokeColor, strokeOpacity, and debug.  This should be turned into an example that also tests number of lines.
manthey added a commit that referenced this issue Nov 21, 2016
This resolves issues #204 and #596.

Previously, lines were not rendered correctly for several conditions: (a) miter locations were sometimes miscalculated, resulting in inconsistent line widths.  (b) when the miter limit was exceeded, an attempt to prevent an excessively long line produced skewed segments.  (c) On the inside of acute angles on short line segments, some of the triangles used to render lines were twisted.  (d) when lines overlap, the overlap was inconsistent.

All of this has been fixed.  Additionally, we now support:
- line caps: butt (default and what was avaulable before), round, square.  These can vary by point.
- line joins: miter (default), round, bevel, miter-clip.  These can vary by point.  Miter and miter-clip have a configurable miter limit (one value for the whole feature) which, if the miter would be longer than this, switches to using a bevel or a clipped miter.
- antialiasing on edges and end caps.  This can be specified for the whole feature.
- strokeOffset: This can vary from -1 (shift left) to 1 (shift right), where 0 is the line centered on the vertices, and 1 is with the edge of the line on the vertices.

All of this is done by rendering two triangles per line segment and performing much of the computation in the vertex and fragment shaders.

There are a few limitations to the current implementation:
- there is finite precision in the calculations, resulting in occasional artifacts along miter joins.  These are mostly only noticeable on very wide lines using stroke offsets, and, even then, are subtle.
- When the line width changes per vertex, sometimes the inside joins are not technically correct.  Instead of using the angle of the lines meeting, the angle of the edges of the lines meeting would need to be calculated.
- On short line segments, one edge of the miter can be by itself, which would ideally either be antialiased or wouldn't be trimmed.  Currently, on wide short lines with multiple segments, the mitered edge can look blocky in limited instances.
- When a line segment has zero length in screen space, it is not drawn at all.  Adjacent line segments may not be drawn exactly as expected.  Also, if line caps are used, perhaps zero-length segments should still be visible.

This method uses 18/15 as much memory as lines used before.

There is a debug flag to show all of the pixels sent to the fragment shader.  If debug is not specified when the feature is created, the debug code is not even compiled into the fragment shader.

There could be some efficiency improvements in the shaders.  For instance, it might worth it to have a quick test for simple line segments without joins.

The new features and changes are exposed in one of the selenium tests (test/selenium/glLines/?wide=true), which can take a variety of query options, such as strokeOffset, lineCap, lineJoin, miterLimit, antialiasing, strokeWidth, strokeColor, strokeOpacity, and debug.  This should be turned into an example that also tests number of lines.
@manthey
Copy link
Contributor Author

manthey commented Jan 20, 2017

Fixed in PR #649.

@manthey manthey closed this as completed Jan 20, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants