pretty

Wednesday 25 September 2013

Draw beautiful font outlines with shaders


1. Introduction.

In this post I will describe how to add font outlines to anti-alised fonts using GLSL shaders. The described approach is not based on commonly used signed distance fields method. All that is needed is a font characters textures and quads to render them on.




2. Algorithm.

The process of adding outline to a character consists of two steps.

1. Make a character bolder.
2. Make this newly acquired boldness to be an outline.

This will be done in a fragment shader. The vertex shader will only fill in a current texture coordinate to be used in fragment shader.
const char* VertexShader = "void main()\
{\
    gl_TexCoord[0] = gl_MultiTexCoord0;\
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\
}";
Now for the fragment shader.
const char* FragmentShader = "uniform vec4 FontColor;\
    uniform vec4 OutlineColor;\
    uniform sampler2D Tex;\
\           
void main()\
{\
vec2 TexCoord = gl_TexCoord[0].st;\
vec2 Offset = 1.0 / textureSize(Tex, 0);\
\
vec4 n = texture2D(Tex, vec2(TexCoord.x, TexCoord.y - Offset.y));\
vec4 e = texture2D(Tex, vec2(TexCoord.x + Offset.x, TexCoord.y));\
vec4 s = texture2D(Tex, vec2(TexCoord.x, TexCoord.y + Offset.y));\
vec4 w = texture2D(Tex, vec2(TexCoord.x - Offset.x, TexCoord.y));\
\
vec4 TexColor = texture2D(Tex, TexCoord);\
float GrowedAlpha = TexColor.a;\
GrowedAlpha = mix(GrowedAlpha, 1.0, s.a);\
GrowedAlpha = mix(GrowedAlpha, 1.0, w.a);\
GrowedAlpha = mix(GrowedAlpha, 1.0, n.a);\
GrowedAlpha = mix(GrowedAlpha, 1.0, e.a);\
\
vec4 OutlineColorWithNewAlpha = OutlineColor;\
OutlineColorWithNewAlpha.a = GrowedAlpha;\
vec4 CharColor = TexColor * FontColor;\
\
gl_FragColor = mix(OutlineColorWithNewAlpha, CharColor, CharColor.a);\
}";

Fragmend shader 3 inputs: FontColor, OutlineColor (these colors would be set by glUniform4f later on), Tex (the character texture bound to sampler).

In the first two lines we take the current texture coordinate and the minimum offset to neighboring coordinates. Next we take the colors of a 4 neigboring texture fragments, to the north, east, south and west of current location. We would need just the alpha values of these colors in the next block that makes heavy use of GLSL mix() function.

In the subsequent block the character is being nurtured. The coordinate with the alpha value of 0 will remain the same only if all of four neighbours have 0 alpha's. Suppose that the current fragment's alpha is 0 while northern fragment's alpha is 0.7 and eastern 0.1. The image shows that in this case GrowedAlpha value will be 0.73. This newly calculated alpha value is assigned to the outline color's alpha value.


In the final part of this shader we need to assign the new color to the fragment through writing it to gl_FragColor. If the font's texture color in this fragment is more transparent we should output the color that is closer to the outline color, because such fragments are closer to character edges. This is done using again glsl mix() linear interpolation function. It interpolates between character color and outline color so that transition between character color and outline would be smooth, without jaggedness.

Note, fully transparent fragments in character texture will be assigned the color of outline as it is. But the outline's alpha in such fragment would be 0, so fragment will remain fully transparent, as it should.


3. Implementation details.

To generate textures from the TTF fonts I propose to take well-known NeHe's Lesson #43 by Sven Olsen for a test. It uses FreeType library to generate char bitmaps from the given font, then it puts these in display lists of textured quads. The text is rendered through glCallLists() function.

First thing we need to do is to put our shader in Lesson43.cpp. I'll use GLEW library to get Open GL extension functions that are necessary for compiling and attaching shaders.
...
#include "GL/glew.h"
GLuint VertexShader, FragmentShader, Program;


void PrintShadeLog(GLuint obj)
{
    int infologLength = 0;
    int charsWritten  = 0;
    char *infoLog;

    glGetShaderiv(obj, GL_INFO_LOG_LENGTH,&infologLength);

    if (infologLength > 0)
    {
        infoLog = (char *)malloc(infologLength);
        glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog);
        printf("%s\n",infoLog);
        OutputDebugString(infoLog);
        free(infoLog);
    }
}

void PrintProgramLog(GLuint obj)
{
    int infologLength = 0;
    int charsWritten  = 0;
    char *infoLog;

    glGetProgramiv(obj, GL_INFO_LOG_LENGTH,&infologLength);

    if (infologLength > 0)
    {
        infoLog = (char *)malloc(infologLength);
        glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog);
        printf("%s\n",infoLog);
        OutputDebugString(infoLog);
        free(infoLog);
    }
}

void CreateShaders()
{
 glewInit();
 
 VertexShader = glCreateShader(GL_VERTEX_SHADER);
 FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
 
 const char* SourceVertexShader = "void main()\
 {\
 gl_TexCoord[0] = gl_MultiTexCoord0;\
 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\
 }";
 
 const char* SourceFragmentShader = "uniform vec4 FontColor;\
 uniform vec4 OutlineColor;\
 uniform sampler2D Tex; \
 void main()\
 {\
 vec2 TexCoord = gl_TexCoord[0].st;\
 vec2 Offset = 1.0 / textureSize(Tex, 0);\
 \
 vec4 n = texture2D(Tex, vec2(TexCoord.x, TexCoord.y - Offset.y));\
 vec4 e = texture2D(Tex, vec2(TexCoord.x + Offset.x, TexCoord.y));\
 vec4 s = texture2D(Tex, vec2(TexCoord.x, TexCoord.y + Offset.y));\
 vec4 w = texture2D(Tex, vec2(TexCoord.x - Offset.x, TexCoord.y));\
 vec4 TexColor = texture2D(Tex, TexCoord);\
 \
 float GrowedAlpha = TexColor.a;\
 GrowedAlpha = mix(GrowedAlpha, 1.0, s.a);\
 GrowedAlpha = mix(GrowedAlpha, 1.0, w.a);\
 GrowedAlpha = mix(GrowedAlpha, 1.0, n.a);\
 GrowedAlpha = mix(GrowedAlpha, 1.0, e.a);\
 \
 vec4 OutlineColorWithNewAlpha = OutlineColor;\
 OutlineColorWithNewAlpha.a = GrowedAlpha;\
 vec4 CharColor = TexColor * FontColor;\
 \
 gl_FragColor = mix(OutlineColorWithNewAlpha, CharColor, CharColor.a);\
 }";
 
 glShaderSource(VertexShader, 1, &SourceVertexShader, NULL);
 glShaderSource(FragmentShader, 1, &SourceFragmentShader, NULL);
 
 glCompileShader(VertexShader);
 glCompileShader(FragmentShader);
 
 PrintShadeLog(VertexShader);
 PrintShadeLog(FragmentShader);
 
 Program = glCreateProgram();
 glAttachShader(Program,VertexShader);
 glAttachShader(Program,FragmentShader);
 glLinkProgram(Program);
 PrintProgramLog(Program);
 glUseProgram(Program);
 
 GLint FontColor = glGetUniformLocation(Program, "FontColor");
 glUniform4f(FontColor, 1.0, 0.0, 0.0, 1.0);
 
 GLint OutlineColor = glGetUniformLocation(Program, "OutlineColor");
 glUniform4f(OutlineColor, 40./255., 215./255., 92./255., 1.0);
}

You can invoke the CreateShaders() function at the end of CreateGLWindow() in Lesson43.cpp.
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
 ...
 CreateShaders();
 return TRUE;
} 
Now running the project you will see something like


The problem with this ugly result is related to a glyph (character) texture size. Depending on a font and specific character, there might be no spare space in texture for the outline. The above image shows the Arial Black italic font with no extra spacing above and below glyphs. We need to make Freetype library to add extra 1-pixel padding for every glyph's texture.

Open the freetype.cpp source file in Lesson43 and add the following function.
void AddPadding(FT_Bitmap& bitmap)
{
 int NewWidth = bitmap.width + 2;
 int NewHeight = bitmap.rows + 2;

 unsigned char* OldBuffer = bitmap.buffer;
 bitmap.buffer = new unsigned char [NewWidth * NewHeight];
 memset(bitmap.buffer, 0, NewWidth * NewHeight);

 for(int j = 0; j < bitmap.rows; j++) 
 {
  for(int i = 0; i < bitmap.width; i++) 
  {
   int IndexNew = (i+1)+((j+1)*NewWidth);
   int IndexOld = i+(j*bitmap.width);

   bitmap.buffer[IndexNew] = OldBuffer[IndexOld];
  }
 }

 bitmap.width = NewWidth;
 bitmap.rows = NewHeight;
}

It should be called from make_dlist() function.

void make_dlist ( FT_Face face, char ch, GLuint list_base, GLuint * tex_base ) {
...

 //This reference will make accessing the bitmap easier
 FT_Bitmap& bitmap=bitmap_glyph->bitmap;

 AddPadding(bitmap); // <-- Add extra 1 pixel padding
                    // on each side of texture
 //Use our helper function to get the widths of
 //the bitmap data that we will need in order to create
 //our texture.
 int width = next_p2( bitmap.width );
...
The result.

1 comment :