memononen / nanovg

Antialiased 2D vector drawing library on top of OpenGL for UI and visualizations.
zlib License
5.15k stars 771 forks source link

Precision of concave shape fill with anti-aliasing #579

Open ghost opened 4 years ago

ghost commented 4 years ago

Am I missing something or is the precision of filling concave shape with anti-aliasing somewhat not very precise ? Here a very simple example:

border-nvg

It has been generated with a code similar to:

    float mx = 50, my = 50, thick = 8;
    nvgPathWinding(vg, NVG_CCW);
    nvgRoundedRect(vg, mx, my, width, height, thick * 3);
    nvgPathWinding(vg, NVG_CW);
    nvgRoundedRect(vg, mx+thick, my+thick, width-thick*2, height-thick*2, thick * 2);
    nvgFill(vg);

The image has been enlarged with a paint program, the guide lines were also added to show what the bounding box of the round rectangles were. All the coordinates were integer.

As you can see, there's a lot of off by 1 :-/

Interestingly, if I shift the rectangles by 0.02 on X and Y, I got this: border-nvg-bias

Even weirder, shifted by 0.5 on X and Y (all coordinates have .5 for their decimal part): border-nvg-bias5

Ouch, anti-aliasing introducing ... aliasing :-/

If I disable anti-alias, the shape are fine and fit perfectly within the bounding box, as well as if I use MSAA or if I use convex shape with anti-aliasing.

Is this the expected behavior ?

memononen commented 4 years ago

Does it work if you reverse the NVG_CCW and NVG_CW? NanoVG handles anti-aliasing by insetting the polygon and rendering 1px wide fade around it. That does not work well with inside-out polygons.

ghost commented 4 years ago

Indeed, that did the trick. Damn I thought I tested this case, but I made another "mistake" (see below). With this code:

    float mx = 50, my = 50, thick = 8;
    nvgPathWinding(vg, NVG_CCW);
    nvgRoundedRect(vg, mx+thick, my+thick, width-thick*2, height-thick*2, thick*2);
    nvgPathWinding(vg, NVG_CW);
    nvgRoundedRect(vg, mx, my, width, height, thick * 3);
    nvgFill(vg);

I know get: nanovg

Which fits perfectly in the bounding box. So drawing the interior CCW and exterior CW instead of exterior CCW and interior CW.

Another mistake I made, was trying to fill a shape like this (as a single path): nanovg_notgood Interior rect was declared CCW and exterior rounded rect was CW (both were drawn using the correct™ way), yet I got off by 1 all over the place.

To prevent this, you need to add an nvgMoveTo command to separate both paths.

And indeed it works with all kind of edge cases:

border

mattkille commented 4 years ago

@tpierron

I like your css border style cases. I was wanting to do something similar myself. I'm curious, and it is not clear from the pictures, does your code handle something like this case correctly, where one side has border-width of zero?

#cornerswithhole {
  border-radius: 40px;
  border-style: solid;
  border-width: 20px 20px 20px 0px;
  padding: 20px;
}

Edit: Actually, on looking closer, I think you may indeed be having the same issue I raised a little while ago: issues/571 Hope to find a nice solution for this without having to switch off AA.

ghost commented 4 years ago

Yes, this is a similar use case than the 3rd box in the last line: border-radius In the screenshot above, there were a few artifacts I didn't noticed, now they are mostly fixed. With your example I got: corner You can see that the pointy edges are not as sharp as a typical browser (the Firefox version is twice as big, because I did this on a HiDPI screen, where 1px CSS = 2px on screen).

I don't think this is such a big deal, because for most of the use cases, the border thickness will be uniform.

On a side note, I had to add this function to nanovg:

void nvgEllipseArc(NVGcontext* ctx, float cx, float cy, float r0, float r1, float a0, float a1, int dir);

Implementation is trivially derived from nvgArc();

mattkille commented 4 years ago

So I'm guessing you are drawing the path+hole as usual, and then correcting the artifact line in some post-processing action? Does this work if drawn over patterned backgrounds?

Edit: oh nevermind, I see that is the result of drawing as a single path. Hmm...

(Note that the artifacts that still remain appear to exist beyond the bounding box. But if you clip that left edge the result will not only be the right size, but also more pointy. A similar approach would be to draw the left border as 1px and then delete.)

If it works for your use case, I agree that it should be good enough. I'm still looking at drawing the whole thing as a single path. Or two separate paths in the case where borders on opposite edges are zero. (I don't need different colours for the borders.)

That nvgEllipseArc() could do with the addition of a rotation parameter, to match the Canvas API. Something along these lines:

void nvgEllipseArc(NVGcontext* ctx, float cx, float cy, float rotation, float r0, float r1, float a0, float a1, int dir)
{
      nvgSave(ctx);
      nvgTranslate(ctx, cx, cy);
      nvgRotate(ctx, rotation);
      nvgScale(ctx, r0, r1);
      nvgArc(ctx, 0, 0, 1, a0, a1, dir);
      nvgRestore(ctx);
}
mattkille commented 4 years ago

This is what I have so far. Quite pointy, with no artifacts. Needed to rearrange the command order and move the inner arcs inwards slightly.

borderarcs