implementation of distance field font rendering

2.3k views Asked by At

It is such a shame that I never gotten a solid font rendering system on my application never before. So I decided to put an end to this.

While I was searching for font rendering examples, I happened to step into this valve paper that everyone compliment.

Yet I lack experience and knowledge to complete this.

Here is the summary of what I did:

First, using Freetype2 library, I loaded a character('A') into the memory as a bitmap.

unsigned char u8Character = 'A';
int32_t u32Character = 0;
utf8::utf8to32(&u8Character, &u8Character + 1, &u32Character);
FT_Set_Char_Size(face, 0, 64 * 64, 300, 300);    
FT_Load_Char(face, u32Character, FT_LOAD_RENDER);

Second, I computed distance field as chebyshev distance from every pixel of the bitmap to generate floating-point number map.

Note: Honestly, I don't know how to do this. The algorithm below is complete guessing.

template <typename input_type, typename output_type>
output_type chebyshev_distance( input_type from_x
                              , input_type from_y
                              , input_type to_x
                              , input_type to_y )
{
    input_type dx = std::abs(to_x - from_x);
    input_type dy = std::abs(to_y - from_y);
    return static_cast<output_type>(dx > dy ? dx : dy);
}
void GenerateSigendDistanceFieldFrom( const unsigned char* inputBuffer
                                    , int width
                                    , int height
                                    , float* outputBuffer
                                    , bool normalize = false)
{
    for (int iy = 0; iy < height; ++iy)
    {
        for (int ix = 0; ix < width; ++ix)
        {
            int index = iy*width + ix;
            unsigned char value = inputBuffer[index];
            int indexMax = width*height;
            int indexMin = 0;
            int far = width > height ? width : height;
            bool found = false;
            for (int distance = 1; distance < far; ++distance)
            {
                int xmin = (ix - distance) >= 0 ? ix - distance : 0;
                int ymin = (iy - distance) >= 0 ? iy - distance : 0;
                int xmax = (ix + distance) < width ? ix + distance+1 : width;
                int ymax = (iy + distance) < height ? iy + distance+1 : height;
                int x = xmin;
                int y = ymin;
                auto fCompareAndFill = [&]() -> bool
                {
                    if (value != inputBuffer[y*width + x])
                    {
                        outputBuffer[index] = chebyshev_distance<int, float>(ix, iy, x, y);
                        if (value < 0xff/2) outputBuffer[index] *= -1;
                        //outputBuffer[index] = distance;
                        return true;
                    }
                    return false;
                };
                while (x < xmax)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    ++x;
                }
                --x;
                if (found == true){ break; }
                while (y < ymax)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    ++y;
                }
                --y;
                if (found == true){ break; }
                while (x >= xmin)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    --x;
                }
                ++x;
                if (found == true){ break; }
                while (y >= ymin)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    --y;
                }
                if (found == true){ break; }

            } // for( int distance = 1; distance < far; ++distance )

        } // for( int ix = 0; ix < width; ++ix )

    } // for( int iy = 0; iy < height; ++iy )

    if( normalize )
    {
        float min = outputBuffer[0];
        float max = outputBuffer[0];
        for( int i = 0; i < width*height; ++i )
        {
            if( outputBuffer[i] < min )
                min = outputBuffer[i];
            if( outputBuffer[i] > max )
                max = outputBuffer[i];
        }
        float denominator = (max - min);
        float newMin = min / denominator;
        for( int i = 0; i < width*height; ++i )
        {
            outputBuffer[i] /= denominator;
            outputBuffer[i] -= newMin;
        }
    }

} // GenerateSigendDistanceFieldFrom

Third, as a test, I rendered this texture of a character to the whole screen with size 800x600 to see how they expand. Texture was sampled "GL_LINEAR" The result was awful.

!1! Render Alpha as red value.

const GLchar fssource[] =
    "#version 440 \n"
    "out vec4 v_color;"
    "in vec2 v_uv;"
    "uniform sampler2D u_texture;"
    "void main()"
    "{"
    "   float mask = texture(u_texture, v_uv).a;"
    "   v_color = vec4(mask,0,0,1);"
    "}"
;

enter image description here

!2! Render Text. threshold for alpha is 0.5

const GLchar fssource[] =
    "#version 440 \n"
    "out vec4 v_color;"
    "in vec2 v_uv;"
    "uniform sampler2D u_texture;"
    "void main()"
    "{"
    "   vec4 result = vec4(1,1,1,1);"
    "   float mask = texture(u_texture, v_uv).a;"
    "   if( mask >= 0.5 ) { result.a = 1; }\n"
    "   else { result.a = 0; }\n"
    "   v_color = result;"
    "}"

enter image description here

!3! Render Text. threshold for alpha is 0.7

const GLchar fssource[] =
    "#version 440 \n"
    "out vec4 v_color;"
    "in vec2 v_uv;"
    "uniform sampler2D u_texture;"
    "void main()"
    "{"
    "   vec4 result = vec4(1,1,1,1);"
    "   float mask = texture(u_texture, v_uv).a;"
    "   if( mask >= 0.7 ) { result.a = 1; }\n"
    "   else { result.a = 0; }\n"
    "   v_color = result;"
    "}"

enter image description here

The font looks bumpy, and apparently distance field is too bright. the algorithm is supposed to work with 0.5 threshold. Not only the result is incorrect, the generation of distance field takes too much time. Thus, I couldn't use high resolution image as a input.

Here I'm doing something clearly wrong, but it seems I'm on my own to figure out how to generate correct result.

But please, help me if you know what I'm doing wrong.

Below is entire source file:

#include <iostream>
#include <iomanip>
#include <algorithm>
#include <fstream>
#include <vector>
#include <cstdint>
#include <climits>
#include "ft2build.h"
#include FT_FREETYPE_H
#include "../utf8_v2_3_4/Source/utf8.h"
#include <SDL.h>
#include "../Glew/glew.h"
#include <gl/GL.h>
#undef main
template <typename input_type, typename output_type>
output_type chebyshev_distance( input_type from_x
                              , input_type from_y
                              , input_type to_x
                              , input_type to_y )
{
    input_type dx = std::abs(to_x - from_x);
    input_type dy = std::abs(to_y - from_y);
    return static_cast<output_type>(dx > dy ? dx : dy);
}
void GenerateSigendDistanceFieldFrom( const unsigned char* inputBuffer
                                    , int width
                                    , int height
                                    , float* outputBuffer
                                    , bool normalize = false)
{
    for (int iy = 0; iy < height; ++iy)
    {
        for (int ix = 0; ix < width; ++ix)
        {
            int index = iy*width + ix;
            unsigned char value = inputBuffer[index];
            int indexMax = width*height;
            int indexMin = 0;
            int far = width > height ? width : height;
            bool found = false;
            for (int distance = 1; distance < far; ++distance)
            {
                int xmin = (ix - distance) >= 0 ? ix - distance : 0;
                int ymin = (iy - distance) >= 0 ? iy - distance : 0;
                int xmax = (ix + distance) < width ? ix + distance+1 : width;
                int ymax = (iy + distance) < height ? iy + distance+1 : height;
                int x = xmin;
                int y = ymin;
                auto fCompareAndFill = [&]() -> bool
                {
                    if (value != inputBuffer[y*width + x])
                    {
                        outputBuffer[index] = chebyshev_distance<int, float>(ix, iy, x, y);
                        if (value < 0xff/2) outputBuffer[index] *= -1;
                        //outputBuffer[index] = distance;
                        return true;
                    }
                    return false;
                };
                while (x < xmax)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    ++x;
                }
                --x;
                if (found == true){ break; }
                while (y < ymax)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    ++y;
                }
                --y;
                if (found == true){ break; }
                while (x >= xmin)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    --x;
                }
                ++x;
                if (found == true){ break; }
                while (y >= ymin)
                {
                    if (fCompareAndFill())
                    {
                        found = true;
                        break;
                    }
                    --y;
                }
                if (found == true){ break; }

            } // for( int distance = 1; distance < far; ++distance )

        } // for( int ix = 0; ix < width; ++ix )

    } // for( int iy = 0; iy < height; ++iy )

    if( normalize )
    {
        float min = outputBuffer[0];
        float max = outputBuffer[0];
        for( int i = 0; i < width*height; ++i )
        {
            if( outputBuffer[i] < min )
                min = outputBuffer[i];
            if( outputBuffer[i] > max )
                max = outputBuffer[i];
        }
        float denominator = (max - min);
        float newMin = min / denominator;
        for( int i = 0; i < width*height; ++i )
        {
            outputBuffer[i] /= denominator;
            outputBuffer[i] -= newMin;
        }
    }

} // GenerateSigendDistanceFieldFrom


namespace
{
    SDL_Window* window = NULL;
    SDL_Surface* screenSurface = NULL;
    FT_Library freetype;
    FT_Face face;
    SDL_GLContext glContext;
    GLuint glProgram = 0;
    GLuint vbo = 0;
    GLuint vao = 0;
    GLuint glTexture = 0;
}

GLuint MakeShader( GLenum shaderType, const char* source, int slen )
{
    auto shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, (const GLchar**)&source, &slen);
    glCompileShader(shader);
    GLint success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (success == GL_FALSE)
    {
        std::vector<GLchar> glInfoLogBuffer;
        int len;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
        glInfoLogBuffer.resize(len + 1);
        GLsizei outlen;
        glGetShaderInfoLog(shader, glInfoLogBuffer.size(), &outlen, glInfoLogBuffer.data());
        glInfoLogBuffer.back() = 0;
        std::cout << glInfoLogBuffer.data() << std::endl;
        return 0;
    }
    return shader;
}

GLuint MakeProgram( GLuint vshader, GLuint fshader )
{
    auto program = glCreateProgram();
    glAttachShader(program, vshader);
    glAttachShader(program, fshader);
    glLinkProgram(program);
    GLint success;
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if( success == GL_FALSE )
    {
        int len;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
        std::vector<GLchar> buffer;
        buffer.resize(len+1);
        buffer.back() = 0;
        glGetProgramInfoLog(program, buffer.size(), &len, buffer.data());
        std::cout << buffer.data() << std::endl;
        return 0;
    }
    return program;
}

int Initialize()
{
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cout << "SDL could not initialize!";
        return -1;
    }

    window = SDL_CreateWindow("My Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 800, 600, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
    if (!window)
    {
        std::cout << "Window could not be created!";
        return -1;
    }
    screenSurface = SDL_GetWindowSurface(window);
    SDL_FillRect(screenSurface, 0, SDL_MapRGB(screenSurface->format, 0xFF, 0xFF, 0xFF));
    SDL_UpdateWindowSurface(window);


    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    glContext = SDL_GL_CreateContext(window);

    SDL_GL_SetSwapInterval(1);

    GLenum glError = glewInit();
    if( glError != GLEW_OK )
    {
        std::cout << "Failed to initialize glew" << std::endl;
        return -1;
    }

    // ----------------------
    // Vertex Shader
    const GLchar vssource[] =
        "#version 440 \n"
        "layout(location=0) in vec3 a_position;"
        "layout(location=1) in vec2 a_uv;"
        "out vec2 v_uv;"
        "void main()"
        "{"
        " gl_Position = vec4(a_position,1);"
        " v_uv = a_uv;"
        "}\n"
    ;
    auto vshader = MakeShader(GL_VERTEX_SHADER, vssource, _countof(vssource));
    // --------------------
    // Fragment Shader
    //const GLchar fssource[] =
    //    "#version 440 \n"
    //    "out vec4 v_color;"
    //    "in vec2 v_uv;"
    //    "uniform sampler2D u_texture;"
    //    "void main()"
    //    "{"
    //    "   float mask = texture(u_texture, v_uv).a;"
    //    "   v_color = vec4(mask,0,0,1);"
    //    "}"
    //;
    const GLchar fssource[] =
        "#version 440 \n"
        "out vec4 v_color;"
        "in vec2 v_uv;"
        "uniform sampler2D u_texture;"
        "void main()"
        "{"
        "   vec4 result = vec4(1,1,1,1);"
        "   float mask = texture(u_texture, v_uv).a;"
        "   if( mask >= 0.7 ) { result.a = 1; }\n"
        "   else { result.a = 0; }\n"
        "   v_color = result;"
        "}"
    ;
    auto fshader = MakeShader(GL_FRAGMENT_SHADER, fssource, _countof(fssource));

    // --------------------
    // Shader Program
    glProgram = MakeProgram( vshader, fshader );

    // --------------------
    // Vertex Buffer Object
    float vb[] =
    {
        -1, -1, 0,
        1,  -1, 0,
        -1, 1,  0,

        1,  -1, 0,
        1,  1,  0,
        -1, 1,  0,

        0, 0,
        1, 0,
        0, 1,

        1, 0,
        1, 1,
        0, 1,
    };
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vb), vb, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // --------------------
    // Vertex Array Object
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (const GLvoid*)(sizeof(float)*18));
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    //
    // Freetype
    //

    FT_Error error = FT_Init_FreeType(&freetype);
    if (error)
    {
        std::cout << "FreeType: error occured with error code: " << error << std::endl;
    }
    error = FT_New_Face(freetype, "C:/Windows/Fonts/Arial.ttf", 0, &face);
    if (error)
    {
        std::cout << "FreeType: error occured with error code: " << error << std::endl;
    }
    error = FT_Set_Char_Size(face, 0, 64 * 64, 300, 300);
    if (error)
    {
        std::cout << "FreeType: error occured with error code: " << error << std::endl;
    }

    unsigned char u8Character = 'A';
    int32_t u32Character = 0;
    utf8::utf8to32(&u8Character, &u8Character + 1, &u32Character);
    error = FT_Load_Char(face, u32Character, FT_LOAD_RENDER);
    if (error)
    {
        std::cout << "FreeType: error occured with error code: " << error << std::endl;
    }

    auto bitmap = face->glyph->bitmap;
    const int width = bitmap.width;
    const int height = bitmap.rows;
    const int size = width*height;
    std::vector<float> outputBuffer;
    outputBuffer.resize(size);
    GenerateSigendDistanceFieldFrom(face->glyph->bitmap.buffer, width, height, outputBuffer.data(), true);

    std::ofstream ofs("testout.txt");
    for (int i = 0; i < height; ++i)
    {
        for (int j = 0; j < width; ++j)
        {
            ofs << bitmap.buffer[i*width + j] << ' ';
        }
        ofs << std::endl;
    }
    ofs << std::endl;
    for (int i = 0; i < height; ++i)
    {
        for (int j = 0; j < width; ++j)
        {
            ofs << std::setw(6) << std::setprecision(2) << std::fixed << outputBuffer[i*width + j];
        }
        ofs << std::endl;
    }

    // ----


    // --------------------
    // Texture
    std::vector<float> invertY;
    invertY.reserve(size);
    for( int i = height-1; i >= 0; --i )
    {
        for( int j = 0; j < width; ++j )
        {
            invertY.push_back(outputBuffer[i*width+j]);
        }
    }
    glGenTextures(1, &glTexture);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, glTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_FLOAT, invertY.data());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);


    glClearColor(0.5f,0.5f,1.0f,1.0f);

    return 0;
}
void Release()
{
    SDL_DestroyWindow(window);

    SDL_Quit();
}
int main(int argc, char* argv[])
{
    Initialize();
    bool quit = false;
    while( !quit )
    {
        SDL_Event e;
        if( SDL_PollEvent(&e) != 0 )
        {
            if (e.type == SDL_QUIT)
            {
                quit = true;
            }
        } // if( SDL_PollEvent(&e) != 0 )

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glUseProgram(glProgram);
        GLint loc = glGetUniformLocation(glProgram, "u_texture");
        glUniform1i(loc, 0);
        glBindVertexArray(vao);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);
        glUseProgram(0);

        SDL_GL_SwapWindow(window);
    } // while( !quit )
    Release();
    return 0;
}
2

There are 2 answers

1
marom On

If I understood all correctly you set the bitmap char size to 64 (=64.64 in Freetype 26.6 fixed point type) but then you need to stretch the bitmap to a larger size, hence the scaling.

I suggest you to set the char size (with FT_Set_Char_Size) to a dimension equal or bigger than final size. Then the remaining of the SW should just keep the bitmap as is or, eventually, downsize it. This does not imply loss of quality, while the upscaling (turning a raster image of size x to a bigger size) takes you to the observed problems. Then for downscaling any interpolation scheme will give you decent results.

0
Botond Máté On

To get a smooth edge you need to be using smoothstep around the edge, maybe from 0.5 alpha to 0.6. As to the signed distance field taking too much time, you should not be generating the texture at runtime, instead you should generate it beforehand and then just load it in, with all the necessary data stored.