memononen / nanovg

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

How to properly scale stroke? #233

Open danijelz opened 9 years ago

danijelz commented 9 years ago

Currently stroke is scaled as if SVG vector-effect:non-scaling-stroke was applied. If I draw a line with stroke width set to 1.0 and current state xform set to scale 10.0x, 10.0y the line ends up 1px wide instead of 10px. I tryed to handle this by transforming vertices in shader, but I can't get antialiasing to work if X and Y scale components differ too much (e.g. 2.0x, 30.0y). If scaling is consistent along both axes AA works fine.

memononen commented 9 years ago

The stroke should get scaled based on the scale from the transform: https://github.com/memononen/nanovg/blob/master/src/nanovg.c#L2119 It is uniform scaling, though.

danijelz commented 9 years ago

Sorry, I somehow missed that while porting your great library to Java as a backend for SVG renerer which I miss so much in libGDX framework. SVG spec states that stroke should get scaled nonuniformly (more about that shortly).

For now I managed to get couple new things to work and I'd gladly share them if you think that they are usable:

All changes are minor additions to a masterpiece you made, so they can be easily added to nanovg. I'm also planning to add support for clipping which will be managed by stencil. But now I'm stuck with transforming vertices in shader. Filling works almost perfectly (some minor artifacts become visible when ratio between dimensions is too big e.g. 1/15, but I think I'll be able to get rid of that).

Stroking is a bigger problem. As much as I understand how a stroke is rendered, strokeMult uniform pretty much controls everything in the strokeMask function. strokeMult is dependent on stroke width and AA fringe width which should be different for X and Y component when nonuniform scaling is applied (with uniform/semi uniform scale rendering is fine). The result is visible as thicker AA belt on the axis with higher scale factor (the pyramid is wider than one px). Since I'm a complete newbie to GL I have no idea how to make strokeMask method to take nonuniform scale into account. Any help for this or a thought of another way of implementing this (e.g. by surrounding stroke by real fringe made of triangle strip like with fill) would be appreciated.

memononen commented 9 years ago

The stroke is intentionally uniform, as nonuniform has more corner cases to implement. There are two options 1) expand the AA fringe manually like fill (doubles or triples the triangle count), 2) use shader derivatives to find "iso contour" of the gradient. For the second option, this article gives some hints: http://prideout.net/blog/?p=22

If you transform the vertices after flattening (either fill or stroke), you will get artifacts in the AA.

danijelz commented 9 years ago

What kind of artifacts should I expect when transforming after flattening? I'm taking into account scale and distTol is appropriately 'scaled' by bigger scale factor.

Thanks for article. It will keep me busy for next couple of months:).

memononen commented 9 years ago

The "baked in" antialiasing fringe is 1px wide, so scaling it make things too blurry or, when scaling down, you'll loose anti-aliasing.

danijelz commented 9 years ago

I handle that by scaling AA width with proper factor for X and Y component when calculating vertex coordinates. For example in nvg__expandFill rw, lw and woff are replaced by rwx, rwy, lwx, lwy, woffx and woffy and then subbmitted to vertex composing.

For now everything looks great except the stroke's AA belt. I'm still trying to understand how strokeMask could be changed to take some other parameters into account instead of provided strokeMult. If I pass original fpos along with transformed one and find some ratio between them maybe this would help. My biggest problem is that, for me, this is first project with GL and 2D/3D graphics/math. We all have to start somewhere...

memononen commented 9 years ago

The belt, a.k.a. fringe, needs to be expanded normal to the stroke. When you have non-uniform scaling, that is not the case anymore. I might be ok in some cases, but scaling it more is not a general solution.

But... with some Clever-Math (tm), you should be able to adjust the stroke non-uniformly so that some part of the gradient matches the outline, and then use the derivatives to calculate the 1px fringe from that.

danijelz commented 9 years ago

The fringe gets expanded but it is scaled when constructing vertices. For example if scale factor is 10 then fringe width is set to 1/10 and if scale factor is 0.1 then fringe width is set to 10. This is all handled in nvgexpandFill and nvgexpandStroke so that different X and Y scale factors are taken into account. When I compare scaled and nonscaled geometry by drawing one upon other the geometry is perfect. For fill everything works. The problem is with stroke. AA pyramid which is constructed by shader in strokeMask takes into account the stroke width. This is hidden in strokeMult which is calculated as: frag->strokeMult = (width_0.5f + fringe_0.5f) / fringe;

If the scaling is uniform or nearly uniform I can calculate strokeMult such that it does the job perfectly: frag->strokeMult = (width_averageScale_0.5f + fringe*0.5f) / fringe;

But this is not OK if scale components differ too much. As I mentioned, I'm trying to understand strokeMask better so maybe I could 'convince' it to always operate in 1px limit. For this to happen strokeMult should be calculated in the fragment shader so it would reflect change ratio between transformed and original fpos. Of course this is just a gut feeling since I don't have enough experience in graphics...

P.S. sorry if I bother too much

memononen commented 9 years ago

Derivatives. Read up the article I linked above, pay special attention to dFdx, dFdy, and fwidth.

danijelz commented 9 years ago

I tried and I can't understand how those functions could get useful to detect "iso contour" of the gradient. I found some articles about using barycentric coordinates to figure out how close you are to an edge during fragment shading. But this is still hi level stuff for me. After reading all I can find I still don't know if strokeMask should remain in code and be used depending of closeness to edge or should it be abandoned completely. Can you please just type some pseudo code to describe how you would approach to this? Pretty, pretty please....

memononen commented 9 years ago

Unfortunately it's been a while since I've used the derivatives, so I'm equally lost as you are. All I can tell is that that is the direction I'd research towards.

danijelz commented 9 years ago

Thanks for everything.