Rendering UTF-8 Text Using SDF Font

  • Tutorial

Continuing a series of articles on mobile game development. In this article, I will tell you how to render UTF-8 text using SDF Bitmap fonts, how to create these fonts and how to use this technique for high-quality rendering of icons.



Content


Part 1. Mobile cross-platform engine.
Part 2. Rendering of UTF-8 text using SDF font.
Part 3. Rendering of a drop with transparency and reflections.




SDF ( Signed Distance Field ) is a grayscale image generated from a black and white contrast image in which the gray level indicates the distance to the nearest contrasting border. It sounds confusing, but it's really very simple.


The SDF font itself looks like this:



Let's take this image and change its levels in Photoshop or any other image editor.



It looks better already! We have a clear font with anti-aliasing on the edges.
We can also get a bold or thin outline. But get Italic alas does not work .



The main plus of SDF is the ability to increase the font without noticeable artifacts.



I recommend reading more about SDF here .


How to create an SDF font?


First of all, you need to create the most common black and white bitmap font. You can do this in the good old BMFont or in UBFG .


For a good result, generate a font of size 400pt, without smoothing, with indents of 45x45x45x45 and a picture size of 4096x4096. Merging with this size, I advise you to disable it. most likely UBGF will freeze.


We export the image to PNG without transparency, and it is advisable to choose BMFont for the description format (for greater compatibility).



Next we need ImageMagick and the following command:


convert font.png -filter Jinc (+ clone -negate -morphology Distance Euclidean -level 50%, - 50%) -morphology Distance Euclidean -compose Plus -composite -level 43%, 57% -resize 12.5% ​​font.png

At the output we get a picture of 512x512, which will give us a very good result.
From the file with the description we will need to extract the characters in unicode and their position / size (do not forget to divide the coordinates by 8 since we reduced the picture). What characters should be exported, I will tell a little later in the section about UTF-8.


Wait a minute, UBFG has a built-in Distance Field!
Yes there is. But the result is noticeably worse. Perhaps in the updates, the authors of UBFG will fix this.


Shaders for rendering text


Vertex shader to display each letter, character by character:


#ifdef DEFPRECISION
precision mediump float;
#endif
attribute mediump vec2 Vertex;
uniform highp mat4 MVP;
uniform mediump vec2 cords[4];
varying mediump vec2 outTexCord;
void main(){   
    outTexCord=Vertex*cords[3]+cords[2];
    gl_Position = MVP * vec4(Vertex*cords[1]+cords[0], 0.0, 1.0);
}

DEFPRECISION is needed for OpenGL ES.
In cords [1] and cords [0] we pass the position and scale of the character on the screen.
And in cords [2] and cords [3] - the coordinates of the character on the font texture.


Fragment shader


#ifdef DEFPRECISION
precision mediump float;
#endif
varying mediump vec2 outTexCord;
uniform lowp sampler2D tex0;
uniform mediump vec4 color;
uniform mediump vec2 params;
void main(void){
    float tx=texture2D(tex0, outTexCord).r;
    float a=min((tx-params.x)*params.y, 1.0);
    gl_FragColor=vec4(color.rgb,a*color.a);
}

In color, we transmit the color and transparency of the letter.
And through params, we adjust the thickness and smoothing of the edges of the font.


If you can adjust the font thickness, then you can also display a frame!
Fragment text shader with frame :


#ifdef DEFPRECISION
precision mediump float;
#endif
varying mediump vec2 outTexCord;
uniform lowp sampler2D tex0;
uniform mediump vec4 color;
uniform mediump vec4 params;
uniform mediump vec3 borderColor;
void main(void){
    float tx=texture2D(tex0, outTexCord).r;
    float b=min((tx-params.z)*params.w, 1.0);
    float a=clamp((tx-params.x)*params.y, 0.0, 1.0);
    gl_FragColor=vec4(borderColor+(color.rgb-borderColor)*a, b*color.a);
}

Additionally, we pass thickness, anti-aliasing to params.zw and border color to borderColor .
You should get this result:



To get beautiful edges for both small and large text sizes, you need to choose different contrast / smoothing parameters ( params ) for the small font and for the large one. Then interpolate them at their current size.


In my opinion, for small sizes it is well suited:


  • fatter
  • smoother edges
  • the border is minimal and blurry, so as not to ripple

For large size :


  • finer font style
  • the edges are very sharp
  • the border is bigger and sharper

Icons



In modern design, flat icons have become quite popular. Free vector icons full of full . All we need to do is to collect a black and white texture atlas from the necessary icons and in the same way drive it through ImageMagick!


As a result, we can store the icons in a rather low resolution, but get a good result when scaling and rotating the icons!


As a bonus, you can easily add a gradient to the icons. To do this, just hang the colors on the vertices, and we get the gradient due to interpolation between the points. The radial gradient will have to be done pixel by pixel in the shader fragment.


Utf-8


In modern projects, no one uses single-byte encodings. Everyone switched to UTF-8, wchar, unicode. For example, it’s convenient for me to work with strings in UTF-8 char *.
UTF-8 is easy to decode in unicode and fits perfectly with Java / String and NSString.


UTF-8 to Unicode conversion function:


static inline unsigned int UTF2Unicode(const unsigned char *txt, unsigned int &i){
    unsigned int a=txt[i++];
    if((a&0x80)==0)return a;
    if((a&0xE0)==0xC0){
        a=(a&0x1F)<<6;
        a|=txt[i++]&0x3F;
    }else if((a&0xF0)==0xE0){
        a=(a&0xF)<<12;
        a|=(txt[i++]&0x3F)<<6;
        a|=txt[i++]&0x3F;
    }else if((a&0xF8)==0xF0){
        a=(a&0x7)<<18;
        a|=(a&0x3F)<<12;
        a|=(txt[i++]&0x3F)<<6;
        a|=txt[i++]&0x3F;
    }
    return a;
}

Bonus! Change the registry unicode character.
static inline unsigned int uppercase(unsigned int a){
    if(a>=97 && a<=122)return a-32;
    if(a>=224 && a<=223)return a-32;
    if(a>=1072 && a<=1103)return a-32;
    if(a>=1104 && a<=1119)return a-80;
    if((a%2)!=0){
        if(a>=256 && a<=424)return a-1;
        if(a>=433 && a<=445)return a-1;
        if(a>=452 && a<=476)return a-1;
        if(a>=478 && a<=495)return a-1;
        if(a>=504 && a<=569)return a-1;
        if(a>=1120 && a<=1279)return a-1;
    }
    return a;
}
static inline unsigned int lowercase(unsigned int a){
    if(a>=65 && a<=90)return a+32;
    if(a>=192 && a<=223)return a+32;
    if(a>=1040 && a<=1071)return a+32;
    if(a>=1024 && a<=1039)return a+80;
    if((a%2)==0){
        if(a>=256 && a<=424)return a+1;
        if(a>=433 && a<=445)return a+1;
        if(a>=452 && a<=476)return a+1;
        if(a>=478 && a<=495)return a+1;
        if(a>=504 && a<=569)return a+1;
        if(a>=1120 && a<=1279)return a+1;
    }
    return a;
}

UTF-8 Blocks


В большинстве шрифтов, особенно креативных, есть только ascii и latin. Как же быть, если нам нужны, например, символы валют? Особенно актуально для in-app платежей, где какие только валюты не попадаются. Предлагаю следующую схему, которая очень хорошо себя зарекомендовала:



Как узнать какие символы есть в шрифте?


Тут на помощь нам приходит странная штука от Adobe — тада! — пустой шрифт!
Его можно использовать в CSS: font-family: Roboto, Adobe Blank;
Именно так получены таблички из картинки выше. Остается только скопировать нужные куски символов и вставить их в UBFG. В итоге мы получим несколько картинок 512х512, где каждая будет содержать столько символов, сколько в нее влезет.


Что за универсальный шрифт?


There are not so many fonts containing most Unicode characters. I settled on Quivira . At least with currency symbols, he is doing well.


Suppose you add bitmaps for Arabic, Japanese, and Chinese. Quite a lot of pictures will come out. Do not rush to download them all! Wait until you really come across a symbol from this block and load the desired texture.


There is also a catch in that all fonts are of different sizes and different baseline. When switching from font to font, the text will skip. Therefore, for each font, select the parameters of its relative scale and shift in Y. Consider these parameters when rendering each character.


I promised buns!
Catch the finished SDF Quivira font already cut into blocks!


Also popular now: