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

Продовжуємо серію статей про мобільному геймдеве. У цій статті я розповім як рендери UTF-8 текст за допомогою SDF Bitmap шрифтів, як ці шрифти створювати і як використовувати цю техніку для якісної візуалізації іконок.

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


SDF (Signed Distance Field) — це зображення з відтінків сірого, створений з контрастного чорно-білого зображення, в якому рівень сірого кольору означає дистанцію до найближчої контрастною кордону. Звучить заплутано, але насправді все дуже просто.

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

Давайте візьмемо це зображення і змінимо його рівні (levels) в фотошопі або будь-якому іншому графічному редакторі.

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

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

Більш детально про техніку SDF рекомендую почитати тут.
Як створювати SDF шрифт?
Перш за все потрібно створити звичайний чорно-білий bitmap шрифт. Зробити це можна в старому добром BMFont або UBFG.
Для хорошого результату генерує шрифт розміром 400pt, без згладжування, з відступами 45x45x45x45 і розміром картинки 4096x4096. Merging при таких розмірах раджу відключити т. к. швидше за все UBGF зависне.
Експортуємо малюнок в PNG без прозорості, а формат опису бажано вибрати BMFont (для більшої сумісності).

Далі нам знадобиться ImageMagick і наступна команда:
convert font.png -filter Jinc ( +clone -negate -morphology Distance Евклідовому -level 50%,-50% ) -morphology Distance Евклідовому -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.
Має вийти ось такий результат:

Щоб отримати красиві краю як при малих, так і при великих розмірах тексту, треба підібрати різні настройки контрасту/згладжування (params для маленького шрифту і для великого. Потім інтерполювати їх за поточним розміром.

На мій погляд, для маленьких розмірів добре підходить:
  • більш жирне накреслення
  • більш згладжені краї
  • бордюр мінімальний і розмитий, щоб не рябил
великого розміру:
  • більш тонке накреслення шрифту
  • краю дуже різкі
  • бордюр більше і різкіше
Іконки

В сучасному дизайні досить популярними стали плоскі іконки. Безкоштовних векторних іконок повно. Все що нам потрібно зробити — зібрати чорно-білий текстурний атлас з потрібних іконок і точно так само прогнати його через ImageMagick!
У підсумку ми можемо зберігати іконки в досить низькому дозволі, але отримувати хороший результат при скейле і обертанні іконок!
Бонусом можна легко додати до іконок градієнт. Для цього треба просто повісити кольору на вертексы, а градієнт отримаємо за рахунок інтерполяції між точками. Радіальний ж градієнт доведеться робити попіксельно в фрагментом шейдере.
UTF-8
У сучасних проектах ніхто вже не використовує однобайтные кодування. Всі перейшли на UTF-8, wchar, unicode. Мені наприклад зручно працювати з рядками в UTF-8 char*.
UTF-8 легко розкодовується в 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 платежів, де які тільки валюти не попадаються. Пропоную наступну схему, яка дуже добре зарекомендувала себе:

Як дізнатися які символи є в шрифті?
Тут на допомогу нам приходить дивна штука від Adobe — тада! — порожній шрифт!
Його можна використовувати в CSS: font-family: Roboto, Adobe Blank;
Саме так отримані таблички з картинки вище. Залишається тільки скопіювати потрібні шматки символів і вставити їх в UBFG. В результаті ми отримаємо кілька картинок 512х512, де кожна буде містити стільки символів, скільки в неї влізе.
Що за універсальний шрифт?
Шрифтів містять більшість Unicode символів не так вже й багато. Я зупинився на Quivira. Принаймні з символами валют у нього все добре.
Припустимо ви додали битмапы для арабської, японської та китайської мов. Вийде досить багато картинок. Не поспішайте їх все завантажувати! Дочекайтеся коли вам дійсно попадеться символ з цього блоку і подгрузите потрібну текстуру.

Так само є підступ у тому, що всі шрифти різного розміру та різних baseline. При переході з шрифту, шрифт текст буде скакати. Тому для кожного шрифту підберіть параметри відносного скейла і зсуву по Y. Враховуйте ці параметри при рендерінгу кожного символу.
Я обіцяв плюшки!
Ловіть готовий SDF шрифт Quivira вже порізаний на блоки!

Анонс наступної статті: Mainloop — управління fps, обробка тачей/кнопок, виконання тасков в основному потоці, підтримка шарів, стейты програми.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.