- PVSM.RU - https://www.pvsm.ru -

Рендеринг UTF-8 текста с помощью SDF шрифта

Продолжаем серию статей о мобильном геймдеве. В этой статье я расскажу как рендерить UTF-8 текст с помощью SDF Bitmap шрифтов, как эти шрифты создавать и как использовать эту технику для качественного рендеринга иконок.

Рендеринг UTF-8 текста с помощью SDF шрифта - 1

Содержание

Часть 1. Мобильный кроссплатформенный движок [1]
Часть 2. Рендеринг UTF-8 текста с помощью SDF шрифта


Рендеринг UTF-8 текста с помощью SDF шрифта - 2 SDF (Signed Distance Field) — это изображение из оттенков серого, сгенерированное из контрастного черно-белого изображения, в котором уровень серого цвета означает дистанцию до ближайшей контрастной границы. Звучит запутанно, но на самом деле все очень просто.

Сам SDF шрифт выглядит так:

Рендеринг UTF-8 текста с помощью SDF шрифта - 3

Давайте возьмем это изображение и изменим его уровни (levels) в фотошопе или любом другом графическом редакторе.

Рендеринг UTF-8 текста с помощью SDF шрифта - 4

Выглядит уже лучше! У нас есть четкий шрифт со сглаживанием на краях.
Так же мы можем получить жирное или тонкое начертание. А вот получить Italic увы не получится.

Рендеринг UTF-8 текста с помощью SDF шрифта - 5

Самый главный плюс SDF — это возможность увеличивать шрифт без заметных артефактов.

Рендеринг UTF-8 текста с помощью SDF шрифта - 6

Более подробно о технике SDF рекомендую почитать тут [2].

Как создавать SDF шрифт?

Прежде всего нужно создать самый обычный черно-белый bitmap шрифт. Сделать это можно в старом добром BMFont [3] или в UBFG [4].

Для хорошего результата генерируйте шрифт размером 400pt, без сглаживания, с отступами 45x45x45x45 и размером картинки 4096x4096. Merging при таких размерах советую отключить т.к. скорее всего UBGF зависнет.

Экспортируем картинку в PNG без прозрачности, а формат описания желательно выбрать BMFont (для пущей совместимости).

Рендеринг UTF-8 текста с помощью SDF шрифта - 7

Далее нам понадобится ImageMagick [5] и следующая команда:

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

На выходе мы получим картинку 512x512, которая даст нам в итоге весьма хороший результат.
Из файла с описанием нам нужно будет вытащить символы в unicode и их положение/размер (не забудьте разделить координаты на 8 т.к. мы уменьшали картинку). Какие именно символы надо экспортировать, я расскажу чуть ниже в разделе про UTF-8.

Минутку, в UBFG ведь есть встроенный Distance Field!
Да, есть. Но результат получается заметно хуже. Возможно в обновлениях авторы UBFG это поправят.

Шейдеры для рендеринга текста

Вертексный шейдер для вывода каждой буквы, символ за символом:

#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 нужен для OpenGL ES.
В cords[1] и cords[0] передаем положение и скейл символа на экране.
А в cords[2] и cords[3] — координаты символа на текстуре шрифта.

Фрагментный шейдер

#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=clamp((tx-params.x)*params.y, 0.0, 1.0);
    gl_FragColor=vec4(color.rgb,a*color.a);
}

В color передаем цвет и прозрачность буквы.
А через params регулируем толщину и сглаживание краев шрифта.

Если можно регулировать толщину шрифта, то значит можно выводить и рамку!
Фрагментный шейдер текста с рамкой:

#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=clamp((tx-params.z)*params.w, 0.0, 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);
}

Дополнительно мы передаем толщину, сглаживание в params.zw и цвет рамки в borderColor.
Должен получиться вот такой результат:

Рендеринг UTF-8 текста с помощью SDF шрифта - 8

Рендеринг UTF-8 текста с помощью SDF шрифта - 9Чтобы получить красивые края как при маленьких, так и при больших размерах текста, надо подобрать разные параметры контраста/сглаживания (params) для маленького шрифта и для большого. Затем интерполировать их по текущему размеру.

На мой взгляд, для маленьких размеров хорошо подходит:

  • более жирное начертание
  • более сглаженные края
  • бордюр минимальный и размытый, чтобы не рябил

Для большого размера:

  • более тонкое начертание шрифта
  • края очень резкие
  • бордюр больше и резче

Иконки

Рендеринг UTF-8 текста с помощью SDF шрифта - 10

В современном дизайне довольно популярными стали плоские иконки. Бесплатных векторных иконок полным полно [6]. Все что нам нужно сделать — собрать черно-белый текстурный атлас из нужных иконок и точно так же прогнать его через ImageMagick!

В итоге мы можем хранить иконки в довольно низком разрешении, но получать хороший результат при скейле и вращении иконок!

Бонусом можно легко добавить к иконкам градиент. Для этого надо просто повесить цвета на вертексы, а градиент получим за счет интерполяции между точками. Радиальный же градиент придется делать попиксельно в фрагментом шейдере.

UTF-8

В современных проектах никто уже не использует однобайтные кодировки. Все перешли на UTF-8, wchar, unicode. Мне например удобно работать со строками в UTF-8 char*.
UTF-8 [7] легко раскодируется в unicode и отлично стыкуется с Java/String и NSString.

Ф-ция преобразования UTF-8 в Unicode:

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;
}

Бонус! Изменяем реестр unicode символа.

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

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

Рендеринг UTF-8 текста с помощью SDF шрифта - 11

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

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

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

Шрифтов содержащих большинство Unicode символов не так уж и много. Я остановился на Quivira [9]. По крайней мере с символами валют у него все хорошо.

Рендеринг UTF-8 текста с помощью SDF шрифта - 12Допустим вы добавили битмапы для арабского, японского и китайского языков. Выйдет довольно много картинок. Не спешите их все загружать! Дождитесь когда вам действительно попадется символ из этого блока и подгрузите нужную текстуру.

Так же есть подвох в том, что все шрифты разного размера и разным baseline. При переходе с шрифта на шрифт текст будет скакать. Поэтому для каждого шрифта подберите параметры его относительного скейла и сдвига по Y. Учитывайте эти параметры при рендеринге каждого символа.

Рендеринг UTF-8 текста с помощью SDF шрифта - 13Я обещал плюшки!
Ловите готовый SDF шрифт Quivira [10] уже порезанный на блоки!

Анонс следующей статьи: Mainloop — управление fps, обработка тачей/кнопок, выполнение тасков в основном потоке, поддержка слоев, стейты приложения.

Автор: Apetrus

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ios/118832

Ссылки в тексте:

[1] Мобильный кроссплатформенный движок: https://habrahabr.ru/post/282065/

[2] тут: https://habrahabr.ru/post/215905/

[3] BMFont: http://www.angelcode.com/products/bmfont/

[4] UBFG: https://github.com/scriptum/UBFG

[5] ImageMagick: http://www.imagemagick.org

[6] полным полно: http://www.flaticon.com/

[7] UTF-8: https://ru.wikipedia.org/wiki/UTF-8

[8] пустой шрифт: https://github.com/adobe-fonts/adobe-blank

[9] Quivira: http://www.quivira-font.com

[10] SDF шрифт Quivira: https://drive.google.com/open?id=0B0t4UF9U4eRzVXFOMGlheV8xTDA

[11] Источник: https://habrahabr.ru/post/282191/