In most cases, it's probably faster, not to mention simpler, to draw round caps/joins as square caps/miters and mask out the round part in the fragment shader (mess with alpha, or use discard if you don't care about early Z). The current approach creates a lot of sliver triangles, which will result in unnecessary vertex and fragment shader invocations. As an added bonus, you can easily implement some amount of antialiasing for the round caps/joins.
But sliver triangles avoid overdraw and thus reduce, not increase, fragment shader invocations? Avoiding overdraw in alpha blending situations is the point of this article.
Last time I rendered this kind of thing, I just did it the simple way you describe so you're not wrong. I just used stencil to prevent alphablending overdraw issues. If you can afford it, it's the simplest and actually the most flexible approach, as it works with any topology, including multiple overlapping segments (e.g. curves, X- and T-shapes, etc).
As patrick said in another reply, this has not been true for a while. On all modern GPU hardware I know of, sliver triangles fall into the category of "degenerate triangles", where the hardware by necessity wastes a ton of time and energy shading pixels that can't ever hit the framebuffer.
You can partially avoid this with early-Z and early-stencil (which will reject entire groups of pixels), but those will only help if most or all of your degenerate triangle is invisible. If a single pixel passes that early culling, it will have to shade at least 4 pixels to produce that one output pixel.
If you are concerned about the cost of drawing a quad where 25-50% of it is transparent/discarded, you can create a simple hull out of 2-4 triangles where it suffers less from the overhead of shading those 2x2 groups. That is a common compromise that can produce improved performance by skipping 2x2 groups that don't contribute to the output frame.
In practice all quads suffer from this, and the larger the quad the more cycles you waste shading the seam between the two triangles twice. The overhead for large quads is still pretty small in comparison, but if you're doing full-screen post-processing effects it can produce a measurable speed-up to draw a single huge (viewport clipped) triangle instead.
It's possible some hardware has optimizations to mitigate this, but it's also the case that since shaders can do things like read/write from arbitrary buffers, any optimization that avoids shading the seam twice would cause incorrect differences in behavior, so it's hard to do that optimization in all cases.
Fragment shader invocations are minimum 2x2 pixel blocks. Portions of those 2x2 pixel blocks outside the primitive represent wasted computation. Sliver triangles, with their long edges and small sizes, tend to increase the number of wasted helper invocations.
How do you handle two lines drawn on top of one another?
The first line sets a bunch of pixels in the stencil buffer that somehow then need to get unset before drawing the next line, no?
I'm actually really interested in this as I'm having a really complicated time doing what should be relatively simple 2D CAD-type stuff with any of the modern graphics APIs (Vulkan or DirectX 12). Any time I want to draw a world-coordinate vertex thing with a device pixel width dimension life just gets horrible.
You could try using the Z buffer instead of the stencil buffer. Give each object a different Z coordinate and use GL_LESS/GL_GREATER depth tests to avoid overdrawing.
It shouldn't if you submit the paths from back to front and enable blending. You can then use analytic AA (distance-based or whatever you'd like) to blend with paths behind the one you're currently drawing.
I think this is still an useful resource beyond shaders, e.g. if you'd like to implement something that renders a "thick" SVG <poly> or <path> out of a polyline.
Yeah, I had exactly this problem before. I tried to wrap my head around how to actually do it geometrically, but thankfully had the much easier idea of just using the blend op.
This is super useful though! You don't always have the ability to change the blending op, or you could be rendering on top of something that might give you artifacts. Thankfully I was rendering something like this on a blank canvas.
That depends on whether you're drawing to a (temporary) buffer with alpha or drawing directly onto a background. Even if you can, it imposes limits on your overall compositing/drawing order.
In the fifth figure, "Adjusting the intersecting vertices of intermediate segments to prevent overlap", you can see that the vertex adjustments cause the angle and thickness of the line segments to change. This looks wrong to me — shouldn't the angle and thickness be unaffected? Shouldn't the red boundaries along the tops and bottoms of the line segments be colinear with the dashed grey boundaries?
I agree, that one seems wrong. From measuring some snapshots of the animating demos though, it looks like they are doing it correctly, as the width doesn't seem to be changing like in that red and grey diagram. I haven't tried to understand the code though.