Comments 39
Не стоит также игнорировать экспорт кернинг-пар — в экзотических шрифтах отсутствие кернинга ой как заметно.
Минутку, в UBFG ведь есть встроенный Distance Field!
Да, есть. Но результат получается заметно хуже. Возможно в обновлениях авторы UBFG это поправят.
Странно. Раньше в алгоритме были ошибки и приближенное вычисление SDF, но в последнем коммите я пытался поправить SDF-генератор и вроде бы он не уступает по качеству BruteForce (см. тест, PSNR > 70 дБ).
Взял последнюю доступную версию для мака 1.1. Вот сравнение:
Видно, что в UBFG радиус размытия больше. И это может быть хорошо, если нужна широкая рамка. Но для ровных краев не подходит. Конечно можно скейлить в 8 раз и в UBFG. Но сделать SDF в UBGF для шрифта размером 400pt занимает очень много времени! ImageMagick пока что лидер по скорости и качеству создания SDF.
Вот какой ф-ционал пригодился бы — возможность задавать резервный шрифт, если символ отсутствует в оригинальном. Сейчас подставляется Arial, если не ошибаюсь? Сперва копал исходники, чтобы самому это исправить, не получилось.
А, всё правильно. Он в отдельной ветке SDF, плюс я не успел новый класс внутрь fontrender-а засунуть.
Резервный шрифт устанавливает операционная система. Надо либо делать свой движок рендеринга ttf, что довольно хлопотно, либо поискать как в ОС поменять резервный Arial на какой-то другой (в linux, например, за это fontconfig отвечает).
В любом случае, спасибо за UBFG! Так же хочу порекомендовать вашу утилиту Cheetah-Texture-Packer для запаковки текстурных атласов. Именно на ее основе я добавил в сборщик проекта автогенерацию атласов из папки.
Допустим вы добавили битмапы для арабского, японского и китайского языков. Выйдет довольно много картинок. Не спешите их все загружать! Дождитесь когда вам действительно попадется символ из этого блока и подгрузите нужную текстуру.С юникодным шрифтом лучше не готовить большие атласы как в вашем архиве. Если в одной строке вдруг встретятся символы из разных атласов — придется переключать текстуры, что достаточно дорого. Особенно актуально это для языков с умляутами. Посему лучше паковать только нужные символы на лету, например как то так: http://www.blackpawn.com/texts/lightmaps/ А при таком подходе вряд ли понадобится больше одной 512*512 текстуры.
Код упаковки хороший, спасибо! Плз поправьте меня, если я не верно понял. SDF шрифт мы все равно делаем заранее (в том числе читаем .fnt файлы) и видимо как-то его все таки режем или здоровенной одной текстурой храним? А потом в рантайме, когда нам попадаются новые символы, добавляем их в кеш-текстуру? Или же заранее смотрим какие у нас будут строки и строим кеш-текстуру? А потом точно так же рендерим текст, но уже из кеш-текстуры. Верно?
Если мы пререндерим символы — то вариантов несколько. Самая простая реализация — создать для каждого символа по файлу, и названия файлам дать скажем хекс кодом символа. Тогда никакого fnt файла не нужно.
Недостатки:
1. Все символы должны быть одной высоты чтобы base line совпадала.
2. Нет кернинговых пар.
3. Куча мелких файлов на винте.
Это все можно легко устранить, для Win например если воспользоваться Compound File. Там можно сохранить и позицию baseline в глифе, и кернинговые пары, и сами битмапы. Ну или fnt сохранять, как сейчас, но как по мне, так проще разобраться с Compound файлами. Получите монолитный блок (один файл), что гарантирует что ничего не перепутается, парсить не надо, доступ кешируемый и т.п.
Варианты с TTF рассматривали? Почему не они? Например, stb_truetype.
Да, конвертирую в WEBP без потери качества "-lossless -q 100".
Остановился на SDF, потому что альтернативы:
а) генерируют битмап шрифт на лету. А это время, ресурсы, зачем? И все равно для хорошего качества атлас должен быть здоровый. Да и бордюров нет.
б) рендерят векторный шрифт. Тут у меня много вопросов к скорости. И опять так бордюры :)
За stb спасибо! Смотрю у них много других полезных библиотек есть.
Ок, прикинем по кол-ву операций и их примерной тяжести:
а) Генерация надписи в FBO и ее последующий вывод без ресайза и контраста.
- Часто надпись динамическая. Например выводим очки юзеру "Score: 142" и цифры бегут. Каждый фрейм изменять FBO будет явно медленнее. К тому же на этом FBO тоже надо шрифт как-то выводить. Замкнутый круг.
- Если же шрифт битмапный статический, то ресайз у него занимает ровно столько же времени, сколько и в SDF. Однако хороший статический битмап будет по размеру раза в два больше (считаем что для хорошего качества на retina нам нужен скейл 2х). А вывод бОльшей текстуры однозначно будет медленнее, к тому же тут будет даунскейл в 2 раза, а в SDF текстура будет примерно совпадать.
- Итак разница только во фрагментном шейдере. Добавили clamp, умножение и сложение. На FPS это никак не повлияло ни на одном из девайсов.
б) Векторный шрифт. Если нам не нужна рамка, то может быть и быстрее будет.
- Однако сложно сказать сколько нужно полигонов для хорошего результата.
- К тому же мы никак не контролируем сглаживание краев и на маленьких размерах получим жуткую рябь.
- Для рамки же придется выводить символ два раза, но! хорошую рамку мы все равно не получим т.к. скейл!=рамка.
Раньше я использовал 2х битмапы, контур добавлял заранее в фотошопе, а разные сочетания цвета шрифта и рамки хранил в разных текстурах. Вот это было глупо и неудобно. Пока не попробовал SDF. При всем старании, минусов в этой технике не вижу.
По ЦП: обычно же шрифт рендерится в буферную текстуру, которая жрёт память, но очень быстро накладывается на экран. При этом значительные затраты ЦП на рендеринг шрифт есть только при загрузке игры, и происходит относительно быстро даже на мобильных девайсах.
Разработчик игры должен сам контролировать это в игровом коде. А если значения x и y в params находятся в диапазоне 0.0 — 1.0, значит clamp не нужен вообще. Я не знаю, как реализован clamp в железе, будет ли это условие или хитрая математика, но лучше уменьшать количество расчетов на каждый пиксель.
params.y — это контраст и значение будет около 10-20. Этот результат мы потом умножаем на прозрачность текста. Вот для чего нужен clamp, иначе выставив прозрачность 50% мы ее не получим. Можно заменить на min(1.0, ...).
Поправил в статье. Однако в шейдере с рамкой один clamp все же нужен, чтобы был цвет рамки/переход/цвет текста.
Обычно это просто модификатор. ALU делает saturate (также как и минус или масштабирование) при записи результата в регистр после какой-либо операции.
Например в описании ISA PowerVR 6 написано
3.4. Modifiers
The arguments to instructions, depending on the instruction, can normally carry a number of modifiers
(Table 3), such as absolute value, negation, floor and others. These “additional” operations incur no
additional cost.
Table 3. Instruction modifiers
.sat Instruction f16/f32: Clamping to [0.f, 1.f]
saturate() это clamp 0..1
https://msdn.microsoft.com/ru-ru/library/windows/desktop/bb509645(v=vs.85).aspx
ret saturate(x)
The x parameter, clamped within the range of 0 to 1.
Я даже на всякий случай проверил. Код:
float uVal;
float4 main(): COLOR0
{
return clamp(uVal, 0.0, 1.0);
}
HLSL компилятор (с O3) компилирует в одну инструкцию:
mov_sat oC0, c0.x
потому что _sat — это модификатор, и на многих железках он ничего не стоит (вам выше процитировали).
А код:
float uVal;
float4 main(): COLOR0
{
return min(uVal, 1.);
}
тот же HLSL компилятор (c O3) комиплирует вот в такую телегу:
def c1, -1, 1, 0, 0
mov r0.x, c0.x
add r0.y, r0.x, c1.x
cmp oC0, r0.y, c1.y, r0.x
Что значительно тяжелее.
А автору статьи в этом месте лучше всего было заюзать saturate вместо clamp-а, чтобы не надятся на оптимизатор, да и код красивее станет.
Самый главный плюс SDF — это возможность увеличивать шрифт без заметных артефактов.
И не только увеличивать, но и вращать, сжимать/растягивать
uniform highp mat4 MVP;
float tx=texture2D(tex0, outTexCord).r;
Почему в этой строке участвует именно r-компонента, а не a?
if(a>=224 && a<=223)return a-32;
(в Бонус! Изменяем реестр unicode символа.)
Рендеринг UTF-8 текста с помощью SDF шрифта