does anyone of you know a good solution to drawing thick line segments with proper end points? Right now, I simply create a GL_LINES mesh and draw it using the fixed function pipeline, but when I specify glLineWidth(8.0f), gaps appear at sharp corners. This is a common problem.
I was hoping one of you would know how to fill the gaps between two segments, either by pre-calculation during mesh generation, or by using a geometry shader. Any help is appreciated.
Couldn't a line segment be represented by a triangle strip? As I imagine it, as you move across the line, you find the normal to it and calculate a "left" and "right" point to it, based on the the thickness that you want.
For straight parts you would just need 4 points and for curved / "corner" parts you would need a higher "sampling".
Edit: I might be wrong, but If you want to use a shader, the tessellation shader is probably more appropriate than the geometry shader.
I remember using a geometry shader for that purpose.
The idea was to pass line_strip to the shader. For each original line you need to pass the previous and the next line as well. Then you output triangle strips by computing the normal and cutting the corners...
I'll try to find this shader later, I think it's on my external hard drive.
@I33N: I would very much appreciate getting a look at your shader.
My current line of thought is to convert the line segments to a GL_TRIANGLES mesh on the CPU, using my own algorithm. I had hoped to find a ready-made piece of code somewhere, but wasn't successful. On paper, the algorithm is finished, I hope to implement it in the coming days. If it works, I will share it with you.
@Safetydank: your link didn't work (the book isn't visible), but searching for "polyline quadstrips" gave me a bunch of new ideas on how to approach this problem. Thanks!
@Ben: also very helpful, I came across that shader before. It even produces 3D lines made of boxes, cool. However, I think with the knowledge I have now I can come up with a 2D version based on triangles myself. I prefer 2D lines over 3D ones for this project, and I also want to run this application on slightly older hardware that doesn't have geometry shader support (read: my computer at home).
I will keep you guys posted. Thanks for helping me out.
Next, I find the miter line (m), which is the normal of the tangent:
Vec2f miter = Vec2f( -tangent.y, tangent.x )
The correct length (d) of the miter can then be found by projecting it on one of the normals (shown in red) using the dotproduct. This gives us a value that is the inverse of the desired length, so:
float length = thickness / miter.dot( normal )
That's all the information we need. Now we can construct the triangle mesh:
The result is quite nice, even for sharp angles:
Note that this algorithm does not always give the desired result at very sharp angles or if the line segment is too short. This can be detected and taken care of, for example by converting the sharp angle to a blunt one by adding an extra point.
I haven't written the TriMesh code yet, and expect some difficulties with texturing (all triangles have different sizes, requiring perspective correction). But I am hopeful I can soon draw awesome 2D textured lines :) Thanks everyone for helping me out.
I managed to write a geometry shader based on the discussion above. It accepts a GL_LINES_ADJACENCY_EXT mesh as input and outputs a smooth line without gaps.
It is slightly different from the explanation given earlier. Instead of calculating the tangent, I simply calculate the 3 normals (previous segment, current segment and next segment) and average 2 of them to find the miter.
Additional code checks for very sharp angles using a dotproduct and performs some naive culling to prevent visual glitches. Shader code can be found in the ZIP file (see end of this post).
At the corners, it extends the line segments to create nice sharp corners that maintain the thickness and appearance of the line.
At very sharp corners, however, it will automatically fall back to normal OpenGL behavior to prevent visual glitches.
Using the shader, you can now (relatively) easily draw proper lines of any thickness. Texturing is not supported at the moment, but it should not be too hard to add it to the shader.
A geometry shader is the only way to go, because I wanted a fixed thickness in pixels. Pre-calculating the mesh in 3D would result in perspective distortion when using a 3D camera. If you would not use a shader, you'd have to recreate the mesh every time the camera moves or zooms and that would put quite a burden on the CPU.
It proved to be a bit tricky doing the conversion from 3D to 2D screen space in the shader. After many unfruitful attempts, I found out it didn't work because I was passing the window size as a Vec2i instead of a Vec2f. This caused the shader to simply not draw anything. So be careful when adopting this piece of code: make sure to properly cast getWindowSize() to a Vec2f when supplying it to the shader!
Very cool exploration. Geometry shaders are nice to work with because you can easily write expressive code while getting much-better-than-CPU performance. Not sure about using them for a more fundamental task like this, though.
I've found that creating lines the same way you create ribbons with GL_TRIANGLES (recommended) or GL_QUADS accomplishes this task. Because the points in each segment are shared with the next, there are no open corners.
the GL_TRIANGLES option would not work for me in this case. I had to draw 2D lines in a 3D world, so had to emulate OpenGL's own fixed-function line drawing as much as possible. Had I used a 3D triangle mesh (where all z-coordinates were 0), the line would become thinner at greater distances and thicker when near the camera. That was not what I wanted.
Since I had to draw in 2D screen space and had to avoid recreating the mesh every time the camera moves, I was forced to use a geometry shader. Apart from that, it was a nice opportunity to learn about geometry shaders in the first place.
Here's a screenshot to compare line drawing without and with the shader (click to enlarge):
As you can see, the map is set in perspective, but the line had to be of the same thickness wherever and however it is drawn. The reason for that is that the map can be zoomed quite a lot, from street-level to viewing the whole country. The line has to be drawn with the same thickness all the time, so it remains clearly visible at every size and does not disappear when zooming out.
Draw a zig-zagging line by placing points with your mouse. Make a few very sharp corners.
Change the miter limit using the Plus (+) and Minus (-) keys and note how some of the corners will get cut off, or not.
Reduce the miter limit to the minimum by repeatedly pressing Minus (-), then switch to the second Geometry shader by pressing F8. The outline will remain the same, but tessellation will be different which in some cases may look better (I personally still prefer the looks of shader 1 (F7)).
Shader 1 will be better for drawing lines with corners, but can't handle very sharp corners well.
Shader 2 will produce smoother curves, but the line will not always be centered on the control points and won't appear to have the same thickness everywhere.
Both shaders are ready for use in your own projects. Let me know what you think.
could you give me some advices how to make it work with line loops, please? i briefly added the first and last 2 vertex indices to the indices vector, but it does not seem right. do i have to create some more adjacency vertices?
I tried to use some of the code you posted but I have problem with WIN_SCALE attribute. I am working with QGLfunctions API which doesn't provide function getWindowSize(). My program is about to draw lines eed smooth th n andick lines.
the getWindowSize() function simply returns the resolution of the window in pixels. You should be able to get that information from somewhere, for example from a window resize event. Note that you need to supply them as floats, not integers, to make the shader work.
this is very nice post. Thank you for sharing it with us. I am working on similar project and I would like to test CPU performance when calculating these points for thick line. Since you omitted part of calculations, do you have the code in C++ perhaps?
actually, the technique was designed specifically for 2D
(screenspace) lines, not for 3D paths. That is, the line always has
the same thickness, regardless the distance from the camera. You'd
have to tweak the shader to use world space coordinates and then it
would also work in 3D.
I've already did it. Unfortunately not yet in form of shader :(
Inspired by your algorithm and mathematics I prepared my own version
of line algorithm, which calculates a part of circle instead of miters.
I needed it to implement it in free, open-sourced therapeutic game made
in unity3d (https://github.com/Motoryka/FriendlyLines),
if you don't mind. I haven't used Yours code