5
$\begingroup$

As some of you may know drawing line segments between sets of points is really difficult especially when you want wide lines or variable widths.

Because I am looking to draw a few hundred of these line segments each frame I am having to go with the cheapest option to achieve a connected segment. So far this has meant that I am using a miter to join the two rectangles like so (described here)

enter image description here enter image description here

The problem with this approach is that the extra length added by the miter is sometimes quite large especially in the case of acute angles and I would like to have rounded corners where possible.

I believe it is a job for the fragment shader as I don't have enough processing resources to calculate more geometry.

Here is a diagram showing what might end up happening after the fragment function:

enter image description here

Purple represents the area between the two lines that ought to be kept; the rest should be removed.

Does anyone know a way to achieve this rounded rectangle in the fragment shader even though texture co-ords are pretty useless in a scenario where the shape is not a polygon.

I wish I was allowed to just draw the rounded circle over the disjointed

Remember, whatever it is it needs to be fast.

$\endgroup$
  • $\begingroup$ If you can pass the coordinates of the original line to the fragment shader, you can try to find the distance from the fragment to the original relevant line segment, and then discard any fragments which are further away than your line thickness. $\endgroup$ – Rotem Nov 1 '17 at 9:03
  • $\begingroup$ "I believe is the job for the fragment shader as I dont have enough processing resources to calculate more geometry." Considering that most line systems of this sort use only the CPU to rasterize them, I rather suspect you have plenty of processing resources to calculate more geometry. $\endgroup$ – Nicol Bolas Nov 1 '17 at 14:32
  • $\begingroup$ @NicolBolas If I were just rendering lines I certainly would but the simulation that creates the lines is quite expensive. $\endgroup$ – J.Doe Nov 1 '17 at 16:36
3
$\begingroup$

First, adressing the concerns of speed and performance, you will always have to make some trade-offs between quality and performance, as you already do. If the first version looks fine enough for your use-case and all other versions are much slower, you have to make the decision yourself. You will, however, not get around actually measuring any time difference. But since various ways to do this are not too hard to integrate into an existing wide lines renderer, just testing the various approaches in action and getting some time measurements shouldn't be too difficult, especially with OpenGL timer queries (or whatever the Direct3D equivalent).

So let's get to some possible improvements to your situation. It seems from your link and your explanations that you're using a classic geometry shader based approach for wide line rendereing, converting each line into a screen-aligned quad (or two triangles), computing the vertex positions based on the line width you want to achieve. I recently built something similar, too and there's a few ways you can improve the quality.

  • As you said yourself, since you're working on rather arbitrary rounded shapes it's probably best to let the fragment shader figure out the exact pixel-shape rather than trying to approximate the circular middle region in the geometry shader. Though, that idea is not completely off-the table and might give for a nice adjustable quality-speed trade-off by adjusting the number of vertices of that circle-part. But it also, together with your current approach, has the problem that it actually needs to know the line's neighbouring lines in order to compute the middle section, which makes it difficult to just put into an existing shader-agnostic rendering system that just draws all the lines as vertex pairs (not to speak of line strips/loops), since you need to add the adjacency information for each line.

  • If you just want to let the fragmet shader do the job using simple alpha testing, you generate a screen-aligned rectangle that's a little larger than the line, i.e. it spans half the width across the endpoints and thus covers the circular end caps, too. In the fragment shader you just check each fragment's screen-space distance to the nearest point on the actual line (which you transfer from the geometry shader to the fragment shader as a varying). And whenever that distance is larger than the line width (or half the line width, to be precise), you discard the fragment. This way you will basically render a rounded box for each line.

    To be precise you render your line as three quad-sections (i.e. 6 triangles in a single strip). The middle section between the ednpoints as one rectangle and the two end-caps as rectangles spanning from the endpoint half the width in the direction of the line. You do this separation to give the rightmost 4 vertices their respective endpoint as varying value and the leftmost one their respective endpoint. This way you get smooth interpolation of the line point across the middle section and clamping in the cap sections.

    This distance computation might also be used to antialias the lines, i.e. by using alpha blending (or multisampling with alpha to coverage) and letting alpha go from 1 to 0 along the outermost ~1-pixel thick border of the line.

    But this approach has the disadvantage of drawing the uneccessary grey area from your above picture, twice even. This not only causes possibly slowing overdraw (and the explicit discarding inhibits early z-testing), it will also interfere with any possible alpha-based antialiasing as described above.

  • A solution to mitigate this could be to actually just draw the completely uncapped lines as rectangles from one endpoint to the other (i.e. like your very first picture). And then drawing the connection points (i.e. the line's vertices) as additional circular large points (i.e. using a similar technique of transforming points into screen-aligned quads with the geometry shader and testing for the distance to the actual point center in the fragment shader to get pixel-perfect circles).

    This has the advantage that you can actually use stencil testing to only draw the connective sections in the regions where you haven't drawn other parts of the line set before and thus only the violet area, which will tremendously reduce overdraw (at the cost of more complicated rendering and per-fragment operations). This gives very nice results with an alpha-based antialiasing approach as described above (there are however still some minor areas where there's some tiny artefacts).

    This stencil testing approach might as well be used on the earlier approach where you draw the line section together with its end-caps when you don't perform any kind of antialiasing or blending, but I think to remember that it caused more artefacts than the separate rendering if you do. I haven't spend too many thoughts on this right now, though, and it might be worth some more investigation. However the seperate solution also has the advantage of requiring the expensive fragment test and possible discard only in the actual connective regions, the line middle sections can be rendered with a straight-forward fragment shader.

Those are just a few thoughts that I happened to ponder on just recently and deemed worth sharing. As said, if they are appropriate for your use-case has to be evaluated, possibly with some timing and quality measurements.

$\endgroup$
  • $\begingroup$ Could you expand a bit more on the 2nd idea? I have used a simple stencil buffer as a mask before but never for something so complicated as telling the GPU not to draw fragments where the same object has already drawn fragments. How exactly would I fill up the stencil buffer? $\endgroup$ – J.Doe Nov 1 '17 at 16:38
  • $\begingroup$ It's really just as simple as that. If you used the stencil buffer for masking before, that's exactly what you do. You clear it to all 0s, then configure the test to succeed only where there's 0 and write a 1 (or whatever) for each fragment that passes. This way every pixel gets written only once. $\endgroup$ – Christian Rau Nov 1 '17 at 16:41
  • $\begingroup$ When using alpha-blending for antialiasing together with the seperated version described last, you should not perform testing for the line middle sections but only fill the stencil buffer, then test against that when drawing the connection points. $\endgroup$ – Christian Rau Nov 1 '17 at 16:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.