memononen / nanovg

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

Text vanishes when transformation is flipped #600

Closed mulle-kybernetik-tv closed 3 years ago

mulle-kybernetik-tv commented 3 years ago

It's easy to reproduce, the Gist has the full modified example file.

      // flip
#if 0
      nvgScale( vg, 1.0, -1.0);
      nvgTranslate( vg, 0,- winHeight);
#endif      
      // triangle
      nvgBeginPath ( vg );
      nvgMoveTo ( vg, winWidth/ 2.0, 0.0);
      nvgLineTo ( vg, winWidth, winHeight );
      nvgLineTo ( vg, 0.0, winHeight);
      nvgLineTo ( vg, winWidth/ 2.0, 0.0);
      nvgFillColor(vg, nvgHSLA(i/19.0f, 0.5f, 0.5f, 255));
      nvgFill(vg);

      // text
      nvgFontFace( vg, "sans");
      nvgFontSize( vg, 64);
      nvgFillColor( vg, nvgRGBA(0,0,0,255)); // TODO: use textColor
      nvgText( vg, winWidth / 2.0, winHeight / 3.0, "AY", NULL);
      nvgEndFrame(vg);

normal

If you enable the flip, then the text vanishes:

flipped

I kinda expected it not to.

mulle-kybernetik-tv commented 3 years ago

I looked at it with qrenderdoc, but I see no problems, except that nothing is displayed. The texture is present, the texture coordinates look to me like they index what they should.

mesh

VTX, IDX, vertex.x, vertex.y, tcoord.x, tcoord.y
0, 0,  499.00,  447.00,  0.00586,  0.00195
1, 1,  542.00,  399.00,  0.08984,  0.0957
2, 2,  542.00,  447.00,  0.08984,  0.00195
3, 3,  499.00,  447.00,  0.00586,  0.00195
4, 4,  499.00,  399.00,  0.00586,  0.0957
5, 5,  542.00,  399.00,  0.08984,  0.0957
6, 6,  540.00,  447.00,  0.09375,  0.00195
7, 7,  581.00,  399.00,  0.17383,  0.0957
8, 8,  581.00,  447.00,  0.17383,  0.00195
9, 9,  540.00,  447.00,  0.09375,  0.00195
10, 10,  540.00,  399.00,  0.09375,  0.0957
11, 11,  581.00,  399.00,  0.17383,  0.0957

The 'AY' is in the bottom right corner where the tccord indexes to (I filled up the rest of the texture with some junk for testing purposes)

texture

So to me it looks OK, but it doesn't work. My suspicion is that the "flip" somehow interferes with the winding/filling logic of OpenGL or nanovg, which I don't fully understand and I don't know how to work around currently.

mulle-kybernetik-tv commented 3 years ago

Aha! When I change the code in nvgText to

      nvgTransformPoint(&c[6],&c[7], state->xform, q.x0*invscale, q.y0*invscale);
      nvgTransformPoint(&c[4],&c[5], state->xform, q.x1*invscale, q.y0*invscale);
      nvgTransformPoint(&c[2],&c[3], state->xform, q.x1*invscale, q.y1*invscale);
      nvgTransformPoint(&c[0],&c[1], state->xform, q.x0*invscale, q.y1*invscale);

basically flipping it also, then the AY appears again in the flipped triangle but in the wrong direction (still upwards)! When I change the vertex creation code texture coordinates to:

     nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
     nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;
     nvg__vset(&verts[nverts], c[2], c[3], q.s1, q.t1); nverts++;

     nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
     nvg__vset(&verts[nverts], c[6], c[7], q.s0, q.t0); nverts++;
     nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;

Then it appears correctly. Now I know why it happens, but I don't know how to correct it, so it works in all cases.

fixed

mulle-kybernetik-tv commented 3 years ago

For my case the following code in nvgText fixes the problem. Not sure about other transformations though.

   if( state->xform[ 3] < 0.0)  // flip vertically ?
   {
      // Transform corners.
      nvgTransformPoint(&c[6],&c[7], state->xform, q.x0*invscale, q.y0*invscale);
      nvgTransformPoint(&c[4],&c[5], state->xform, q.x1*invscale, q.y0*invscale);
      nvgTransformPoint(&c[2],&c[3], state->xform, q.x1*invscale, q.y1*invscale);
      nvgTransformPoint(&c[0],&c[1], state->xform, q.x0*invscale, q.y1*invscale);

      // Create triangles
      if (nverts+6 <= cverts) {
        nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
        nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;
        nvg__vset(&verts[nverts], c[2], c[3], q.s1, q.t1); nverts++;

        nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
        nvg__vset(&verts[nverts], c[6], c[7], q.s0, q.t0); nverts++;
        nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;
      }
   }
   else
   {
      // Transform corners.
      nvgTransformPoint(&c[0],&c[1], state->xform, q.x0*invscale, q.y0*invscale);
      nvgTransformPoint(&c[2],&c[3], state->xform, q.x1*invscale, q.y0*invscale);
      nvgTransformPoint(&c[4],&c[5], state->xform, q.x1*invscale, q.y1*invscale);
      nvgTransformPoint(&c[6],&c[7], state->xform, q.x0*invscale, q.y1*invscale);

      // Create triangles
      if (nverts+6 <= cverts) {
         nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t0); nverts++;
         nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t1); nverts++;
         nvg__vset(&verts[nverts], c[2], c[3], q.s1, q.t0); nverts++;
         nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t0); nverts++;
         nvg__vset(&verts[nverts], c[6], c[7], q.s0, q.t1); nverts++;
         nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t1); nverts++;
      }
   }
mulle-kybernetik-tv commented 3 years ago

I wrote an improved isFlipped check taking some pointers from the way I think paths deal with the same issue.

static int   isTransformFlipped( const float *xform)
{
    float area;
    float c[3*2];

    nvgTransformPoint(&c[0],&c[1], state->xform, 0.0, 0.0);
    nvgTransformPoint(&c[2],&c[3], state->xform, 1.0, 0.0);
    nvgTransformPoint(&c[4],&c[5], state->xform, 1.1, 1.1);

    area = nvg__triarea2( c[0],c[1], c[2],c[3], c[4],c[5]);
    return( area > 0.0);
}   

I wrote a little demo, where I am rotating the triangle and the text is stable in all directions, whether flipped in Y or not. An X or a X/Y flip looks good too.

memononen commented 3 years ago

This is known limitation, and I have not had the time to fix it.

It should be enough to just check the sign of the determinant to see if you need to flip the winding.

float det = t[0] * t[3] - t[2] * t[1];
mulle-kybernetik-tv commented 3 years ago

Yes that works equally as well in my test. The test has to be only done once per nvgText call and not on each glyph fortunately.

The whole nvgText is now:

static inline int   isTransformFlipped( const float *xform)
{
    float det = xform[0] * xform[3] - xform[2] * xform[1];
    return( det < 0);
}

float nvgText(NVGcontext* ctx, float x, float y, const char* string, const char* end)
{
    NVGstate* state = nvg__getState(ctx);
    FONStextIter iter, prevIter;
    FONSquad q;
    NVGvertex* verts;
    float scale = nvg__getFontScale(state) * ctx->devicePxRatio;
    float invscale = 1.0f / scale;
    int cverts = 0;
    int nverts = 0;
    int isFlipped;

    if (end == NULL)
        end = string + strlen(string);

    if (state->fontId == FONS_INVALID) return x;

    fonsSetSize(ctx->fs, state->fontSize*scale);
    fonsSetSpacing(ctx->fs, state->letterSpacing*scale);
    fonsSetBlur(ctx->fs, state->fontBlur*scale);
    fonsSetAlign(ctx->fs, state->textAlign);
    fonsSetFont(ctx->fs, state->fontId);

    cverts = nvg__maxi(2, (int)(end - string)) * 6; // conservative estimate.
    verts = nvg__allocTempVerts(ctx, cverts);
    if (verts == NULL) return x;

    isFlipped = isTransformFlipped( state->xform);

    fonsTextIterInit(ctx->fs, &iter, x*scale, y*scale, string, end, FONS_GLYPH_BITMAP_REQUIRED);
    prevIter = iter;
    while (fonsTextIterNext(ctx->fs, &iter, &q)) {
        float c[4*2];
        if (iter.prevGlyphIndex == -1) { // can not retrieve glyph?
            if (nverts != 0) {
                nvg__renderText(ctx, verts, nverts);
                nverts = 0;
            }
            if (!nvg__allocTextAtlas(ctx))
                break; // no memory :(
            iter = prevIter;
            fonsTextIterNext(ctx->fs, &iter, &q); // try again
            if (iter.prevGlyphIndex == -1) // still can not find glyph?
                break;
        }
        prevIter = iter;
        if( isFlipped)  // flip vertically ?
        {
            // Transform corners.
            nvgTransformPoint(&c[0],&c[1], state->xform, q.x0*invscale, q.y1*invscale);
            nvgTransformPoint(&c[2],&c[3], state->xform, q.x1*invscale, q.y1*invscale);
            nvgTransformPoint(&c[4],&c[5], state->xform, q.x1*invscale, q.y0*invscale);
            nvgTransformPoint(&c[6],&c[7], state->xform, q.x0*invscale, q.y0*invscale);
            // Create triangles
            if (nverts+6 <= cverts) {
                nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
                nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;
                nvg__vset(&verts[nverts], c[2], c[3], q.s1, q.t1); nverts++;

                nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t1); nverts++;
                nvg__vset(&verts[nverts], c[6], c[7], q.s0, q.t0); nverts++;
                nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t0); nverts++;
            }
        }
        else
        {
            // Transform corners.
            nvgTransformPoint(&c[0],&c[1], state->xform, q.x0*invscale, q.y0*invscale);
            nvgTransformPoint(&c[2],&c[3], state->xform, q.x1*invscale, q.y0*invscale);
            nvgTransformPoint(&c[4],&c[5], state->xform, q.x1*invscale, q.y1*invscale);
            nvgTransformPoint(&c[6],&c[7], state->xform, q.x0*invscale, q.y1*invscale);
            // Create triangles
            if (nverts+6 <= cverts) {
                nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t0); nverts++;
                nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t1); nverts++;
                nvg__vset(&verts[nverts], c[2], c[3], q.s1, q.t0); nverts++;

                nvg__vset(&verts[nverts], c[0], c[1], q.s0, q.t0); nverts++;
                nvg__vset(&verts[nverts], c[6], c[7], q.s0, q.t1); nverts++;
                nvg__vset(&verts[nverts], c[4], c[5], q.s1, q.t1); nverts++;
            }
        }
    }

    // TODO: add back-end bit to do this just once per frame.
    nvg__flushTextTexture(ctx);

    nvg__renderText(ctx, verts, nverts);

    return iter.nextx / scale;
}
memononen commented 3 years ago

You could just flip q.y0/1 and q.t0/1, less duplicated code. Would you mind making a PR for this?

Double check your formatting to comply with the rest of the code, and isTransformFlipped should have nvg__ prefix.

mulle-kybernetik-tv commented 3 years ago

I will do a PR tomorrow.