Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage with Freetype #117

Closed
Haeri opened this issue Jan 3, 2021 · 5 comments
Closed

Usage with Freetype #117

Haeri opened this issue Jan 3, 2021 · 5 comments

Comments

@Haeri
Copy link

Haeri commented Jan 3, 2021

First of all, thank you for this great library!

Unfortunately I ran into some alignment and sizing issues that are related to me not correctly understanding all the parameters provided by msdfgen. I first searched the issues and also the msdf-atlas-gen for possible solutions but I wasn't able to find (or understand) the answers.

I am trying to generate a texture containing all glyphs available in the font file. I have already successfully implemented this with FreeType and decided now to upgrade my texture to contain msdfs.

During the upgrade I encountered two issues:

  • When using only FreeType, I had to specify FT_Set_Pixel_Sizes, which then was the baseline of all the pixel metrics FreeType returned like face->glyph->bitmap, face->glyph->bitmap_left etc. Now that I am handing over FreeType with adoptFreetypeFont I am no longer calling FT_Set_Pixel_Sizes since FreeType is no longer doing the rendering and the glyphs are loaded with FT_LOAD_NO_SCALE but this leaves all the necessary glyph data that I need for text alignment empty. Especiall face->glyph->bitmap_left that I use for bearing returns 0. So I am trying to figure out what the appropriate way is to use msdfgen to generate the bitmap whilst FreeType provides the necessary metadata about the glyph. Also how do I find out what the baseline size is, now that I am no longer manually setting the size by FT_Set_Pixel_Sizes ?
  • I have also some trouble aligning the msdf in the bitmap. In my approach, I get the bounds of the shape, and calculate the width and height by subtracting right from left and top from bottom. I also translate the shape by a border amount that I also provide in the getBounds method. Unfortunately, some resulting shapes are cut off.

problem

Here is my code:

FT_Library ft_library;
FT_Face face;

FT_Init_FreeType(&ft_library);
FT_New_Memory_Face(ft_library, data, (FT_Long)size, 0, &face);

// Not calling this anymore
// FT_Set_Pixel_Sizes(face, 32, 32);

unsigned int tex_width = 1024;
unsigned int tex_height = tex_width;
unsigned int buffer_size = tex_width * tex_height * 4;

// Create image buffer
uint8_t* pixels = new uint8_t[buffer_size]{0};
unsigned int pen_x = 0;
unsigned int pen_y = 0;
int max_height = 0;

msdfgen::FontHandle* font_handle = msdfgen::adoptFreetypeFont(face);
msdfgen::FontMetrics font_metrics;
msdfgen::getFontMetrics(font_metrics, font_handle);
_line_height = font_metrics.lineHeight;

FT_UInt gindex = 0;
FT_ULong charcode = FT_Get_First_Char(face, &gindex);

// Go through all available characters in the font
while (gindex != 0)
{
    int w = 0;
    int h = 0;
    double advance = 0;
    float border_width = 4;

    msdfgen::Shape shape;
    msdfgen::Shape::Bounds bounds;

    msdfgen::loadGlyph(shape, font_handle, charcode, &advance);

    if (shape.validate() && shape.contours.size() > 0)
    {
        shape.normalize();
        shape.inverseYAxis = true;

        bounds = shape.getBounds(border_width);

        // Calculate width & height
        w = ceil(bounds.r - bounds.l);
        h = ceil(bounds.t - bounds.b);

        if (max_height < h)
            max_height = h;

        msdfgen::edgeColoringSimple(shape, 3.0);
        msdfgen::Bitmap<float, 3> msdf(w, h);
        msdfgen::generateMSDF(msdf, shape, 4.0, 1.0, msdfgen::Vector2(border_width));

        if (pen_x + msdf.width() >= tex_width)
        {
            pen_x = 0;
            pen_y += max_height;
        }

        for (unsigned int row = 0; row < msdf.height(); ++row)
        {
            for (unsigned int col = 0; col < msdf.width(); ++col)
            {
                int x = pen_x + col;
                int y = pen_y + row;

                pixels[(x + tex_width * y) * 4 + 0] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[0]);
                pixels[(x + tex_width * y) * 4 + 1] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[1]);
                pixels[(x + tex_width * y) * 4 + 2] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[2]);
                pixels[(x + tex_width * y) * 4 + 3] = 255;
            }
        }
    }

//    struct character
//    {
//        vec2 size;      // Size of glyph
//        vec2 bearing;   // Offset from baseline to left/top of glyph
//        vec2 origin;	  // Origin of the texture atlas
//        int advance;    // Horizontal offset to advance to next glyph
//    };

    // face->glyph->bitmap_left and bitmap_top are always 0
    character character = {
        vec2((float)(w), (float)(h)),
        vec2((float)(face->glyph->bitmap_left), (float)(face->glyph->bitmap_top)),
        vec2((float)(pen_x), (float)(pen_y)), ((int)advance >> 6)
    };
    
    _characters[charcode] = character;

    pen_x += w;

    // Next
    charcode = FT_Get_Next_Char(face, charcode, &gindex);
}

msdfgen::destroyFont(font_handle);
FT_Done_Face(face);
FT_Done_FreeType(ft_library);


_texture_atlas = image::create(tex_width, tex_height, 4, pixels, false);
@Chlumsky
Copy link
Owner

Chlumsky commented Jan 3, 2021

I believe that bounds.l is analogous to FreeType's bitmap_left (might need some conversion). You should probably use the bounds in the translation argument of generateMSDF in order to not have characters cut off. I do not know what you mean by baseline size. The baseline is a line, it doesn't have a size, so please clarify. Let me know if any of this helped you at all.

@Haeri
Copy link
Author

Haeri commented Jan 6, 2021

Ok that information was in deed helpful. I found a few errors I made in the original code posted:

  • Instead of supplying brder_width in the translate parameter of generateMSDF I should have used (-bounds.l, -bounds.b) to basically move the glyph back into the center.
  • Also baseline was the wrong word I used. I meant the inherent size of the font face. I discovered that FontMetrics.emSize was what I was looking for.
  • The bearing I was looking for was in deed bounds.l and bounds.t.
  • And lastly my advance was wrong as I was bitshifting it again.

For completeness sake here the adjusted code:

FT_Library ft_library;
FT_Face face;

FT_Init_FreeType(&ft_library);
FT_New_Memory_Face(ft_library, data, (FT_Long)size, 0, &face);

unsigned int tex_width = 1024;
unsigned int tex_height = tex_width;
unsigned int buffer_size = tex_width * tex_height * 4;

// Create image buffer
uint8_t* pixels = new uint8_t[buffer_size]{0};
unsigned int pen_x = 0;
unsigned int pen_y = 0;
int max_height = 0;

msdfgen::FontHandle* font_handle = msdfgen::adoptFreetypeFont(face);
msdfgen::FontMetrics font_metrics;
msdfgen::getFontMetrics(font_metrics, font_handle);
_line_height = font_metrics.lineHeight;

FT_UInt gindex = 0;
FT_ULong charcode = FT_Get_First_Char(face, &gindex);

// Go through all available characters in the font
while (gindex != 0)
{
    float glyph_width = 0;
    float glyph_height = 0;
    double advance = 0;
    float border_width = 4;

    msdfgen::Shape shape;
    msdfgen::Shape::Bounds bounds;

    msdfgen::loadGlyph(shape, font_handle, charcode, &advance);

    if (shape.validate() && shape.contours.size() > 0)
    {
        shape.normalize();
        shape.inverseYAxis = true;

        bounds = shape.getBounds(border_width);

        // Calculate width & height
        glyph_width = ceil(bounds.r - bounds.l);
        h = ceil(bounds.t - bounds.b);

        if (max_height < glyph_height)
            max_height = glyph_height;

        msdfgen::edgeColoringSimple(shape, 3.0);
        msdfgen::Bitmap<float, 3> msdf(glyph_width, glyph_height);
        msdfgen::generateMSDF(msdf, shape, border_width, 1.0, msdfgen::Vector2(-bounds.l, -bounds.b));

        if (pen_x + msdf.width() >= tex_width)
        {
            pen_x = 0;
            pen_y += max_height;
            max_height = 0;
        }

        for (unsigned int row = 0; row < msdf.height(); ++row)
        {
            for (unsigned int col = 0; col < msdf.width(); ++col)
            {
                int x = pen_x + col;
                int y = pen_y + row;

                pixels[(x + tex_width * y) * 4 + 0] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[0]);
                pixels[(x + tex_width * y) * 4 + 1] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[1]);
                pixels[(x + tex_width * y) * 4 + 2] =
                    msdfgen::pixelFloatToByte(msdf(col, row)[2]);
                pixels[(x + tex_width * y) * 4 + 3] = 255;
            }
        }
    }

//    struct character
//    {
//        vec2 size;      // Size of glyph
//        vec2 bearing;   // Offset from baseline to left/top of glyph
//        vec2 origin;	  // Origin of the texture atlas
//        float advance;    // Horizontal offset to advance to next glyph
//    };

    character character = {
        vec2(glyph_width, glyph_height),
        vec2((float)(bounds.l), (float)(bounds.t)),
        vec2((float)(pen_x), (float)(pen_y)), 
        advance
    };
    
    _characters[charcode] = character;

    pen_x += glyph_width;

    // Next
    charcode = FT_Get_Next_Char(face, charcode, &gindex);
}

msdfgen::destroyFont(font_handle);
FT_Done_Face(face);
FT_Done_FreeType(ft_library);


_texture_atlas = image::create(tex_width, tex_height, 4, pixels, false);

As expected, this already gives good results:
Snipaste_2021-01-06_04-55-35
However there seems to be some issuer with very bold or very light font types:
Snipaste_2021-01-06_04-55-48

Is this then a matter of adjusting edgeColoringSimple?
Also can you elaborate a bit what the range parameter actually does in generateMSDF and if it is correct to pair it with border_width? Lastly there are some issues with the icon font I use. As you can see, from the image, some glyphs appear inverted. Is there a way to fix this or is it just a malformed font?

@Chlumsky
Copy link
Owner

Chlumsky commented Jan 6, 2021

It seems that your distance field has either too low resolution or that you indeed used a wrong value for the range parameter. You'd have to post the SDF for me to be able to tell. range determines the width of the range of distances represented as 0 to 255 in the output bitmap. It seems correct to use border_width (or maybe 2 times that? a half?), but you might want to choose that value more carefully, because the units are not pixels.

Finally, for the inverted glyphs, this means that the winding is not consistent. You can try fixing it by calling Shape::orientContours. If this doesn't completely fix it, you'd have to link Skia and use resolveShapeGeometry.

@Haeri
Copy link
Author

Haeri commented Jan 6, 2021

Shape::orientContours seems to have helped a little but there are still some incorrect shapes generated. I have zipped my fonts + the generated msdfs in the zip so there is no compression happening: fonts.zip

I have noticed that there are in general some unpleasant blobs appearing even in the Open Sans font, which on first glance looks fine, but when scaling up shows exactly the thype of blobs one would expect from plain signed distance fields. Is this just the limits of what msdfs can do or am I doing something completely wrong? Would switching to mtsdfs help? Or increasing the resolution? Currently I just use the bounds to determine the bitmap size.
Unbenannt
Unbenannt2
Also I am using the default shader that was provided in the readme:
https://github.com/Haeri/ElementalDraw/blob/26bd8e7664745542f55b9b6b2af5b58c76e642b6/data/shader/rounded_rect.frag#L88

@Chlumsky
Copy link
Owner

Chlumsky commented Jan 6, 2021

As I said, you will need to use the Skia geometry preprocessing to resolve the bad shapes (remove self-intersections).

Yes, your distance fields are way too small. I would at least double the resolution (in both dimensions). You should try different sizes to find the optimal one. As a rule of thumb, the thinnest line should be at least 2 or 3 pixels across in the SDF (so thin fonts tend to require higher resolutions than thick fonts).

You also have some line artifacts because you're sampling the part between individual glyphs, this should be avoided.

I tested your fonts with the atlas generator and got good results, so you should be good to go when you resolve these issues.

@Chlumsky Chlumsky closed this as completed Feb 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants