Pull to refresh

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 текстуры.
Все верно, так и есть. В порезанном шрифте Quivira в одной текстуре помещены ascii и умляуты, что покрывает большинство европейских языков. В других текстурах лежат более специфические символы — отдельно турецкий, греческий и редкие умляуты. Отдельно идет кириллица и отдельно символы, валюты, римские цифры.
Я так понимаю это был ответ мне. Ну та же кириллица например может спокойно встречаться с латинскими буквами и цифрами. Не вижу ни одной причины создавать N атласов, и в рантайме подбирать нужный атлас в зависимости от символа, потому как код, отвечающий за это будет не меньше, чем код, упаковывающий атлас в рантайме. Сходите по моей ссылке, посмотрите. Там реально мало кода.
Дополню свой же ответ: помимо кода, который менеджит N атласов нужен еще код, который парсит информацию о атласах (.fnt файлы в архиве). Если же собирать атлас в рантайме — ничего этого не нужно.

Код упаковки хороший, спасибо! Плз поправьте меня, если я не верно понял. SDF шрифт мы все равно делаем заранее (в том числе читаем .fnt файлы) и видимо как-то его все таки режем или здоровенной одной текстурой храним? А потом в рантайме, когда нам попадаются новые символы, добавляем их в кеш-текстуру? Или же заранее смотрим какие у нас будут строки и строим кеш-текстуру? А потом точно так же рендерим текст, но уже из кеш-текстуры. Верно?

Ну основная идея — получать битмап одного символа, и упаковывать его в атлас. Как получать этот битмап — зависит от того что у вас за проект. Если это игра — то скорее всего лучше заранее пререндерить, потому что необходимый шрифт может оказаться не установленным у конечного пользователя. А в случае скажем с текстовым редактором наоборт, нужно использовать шрифты, которые установленны у пользователя и делать все в реалтайм.
Если мы пререндерим символы — то вариантов несколько. Самая простая реализация — создать для каждого символа по файлу, и названия файлам дать скажем хекс кодом символа. Тогда никакого fnt файла не нужно.
Недостатки:
1. Все символы должны быть одной высоты чтобы base line совпадала.
2. Нет кернинговых пар.
3. Куча мелких файлов на винте.
Это все можно легко устранить, для Win например если воспользоваться Compound File. Там можно сохранить и позицию baseline в глифе, и кернинговые пары, и сами битмапы. Ну или fnt сохранять, как сейчас, но как по мне, так проще разобраться с Compound файлами. Получите монолитный блок (один файл), что гарантирует что ничего не перепутается, парсить не надо, доступ кешируемый и т.п.
PNG-файлы шрифта перед использованием конвертируете в webp как остальные картинки в движке? Или ещё и libpng нужна?
Варианты с TTF рассматривали? Почему не они? Например, stb_truetype.

Да, конвертирую в WEBP без потери качества "-lossless -q 100".
Остановился на SDF, потому что альтернативы:
а) генерируют битмап шрифт на лету. А это время, ресурсы, зачем? И все равно для хорошего качества атлас должен быть здоровый. Да и бордюров нет.
б) рендерят векторный шрифт. Тут у меня много вопросов к скорости. И опять так бордюры :)
За stb спасибо! Смотрю у них много других полезных библиотек есть.

Можно подумать ресайз и контраст бесплатно обходятся. У вас есть замеры скорости или чисто по ощущениям?

Ок, прикинем по кол-ву операций и их примерной тяжести:


а) Генерация надписи в FBO и ее последующий вывод без ресайза и контраста.


  • Часто надпись динамическая. Например выводим очки юзеру "Score: 142" и цифры бегут. Каждый фрейм изменять FBO будет явно медленнее. К тому же на этом FBO тоже надо шрифт как-то выводить. Замкнутый круг.
  • Если же шрифт битмапный статический, то ресайз у него занимает ровно столько же времени, сколько и в SDF. Однако хороший статический битмап будет по размеру раза в два больше (считаем что для хорошего качества на retina нам нужен скейл 2х). А вывод бОльшей текстуры однозначно будет медленнее, к тому же тут будет даунскейл в 2 раза, а в SDF текстура будет примерно совпадать.
  • Итак разница только во фрагментном шейдере. Добавили clamp, умножение и сложение. На FPS это никак не повлияло ни на одном из девайсов.

б) Векторный шрифт. Если нам не нужна рамка, то может быть и быстрее будет.


  • Однако сложно сказать сколько нужно полигонов для хорошего результата.
  • К тому же мы никак не контролируем сглаживание краев и на маленьких размерах получим жуткую рябь.
  • Для рамки же придется выводить символ два раза, но! хорошую рамку мы все равно не получим т.к. скейл!=рамка.

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

Ну то есть бенчмарки не гоняли, ясно.
Что такое FBO?
Не понял, какие проблемы у того же SVG с рамкой?
STB лучше libfreetype? Или они делают не то же самое?
STB зависит только от стандартной библиотеки, соответственно очень портируемо. Лицензия STB разрешает линковаться с этими библиотеками статически даже для коммерческих проектов. STB миниатюрны, например TTF-рендер добавляет к ЕХЕ порядка 40кб при статической линковке. Из недостатков — требуется большее количество ресурсов.
Ресурсы — это только память, или ЦП тоже? К freetype претензии по ЦП есть.
Если найдёте где-нибудь нормальное сравнение, или сделаете сами — не забудьте поделиться с сообществом. У меня такого нет…
По ЦП: обычно же шрифт рендерится в буферную текстуру, которая жрёт память, но очень быстро накладывается на экран. При этом значительные затраты ЦП на рендеринг шрифт есть только при загрузке игры, и происходит относительно быстро даже на мобильных девайсах.
Спасибо за подсказку, что можно сделать со шрифтами. Необычное дело.
> float a=clamp((tx-params.x)*params.y, 0.0, 1.0);

Разработчик игры должен сам контролировать это в игровом коде. А если значения x и y в params находятся в диапазоне 0.0 — 1.0, значит clamp не нужен вообще. Я не знаю, как реализован clamp в железе, будет ли это условие или хитрая математика, но лучше уменьшать количество расчетов на каждый пиксель.

params.y — это контраст и значение будет около 10-20. Этот результат мы потом умножаем на прозрачность текста. Вот для чего нужен clamp, иначе выставив прозрачность 50% мы ее не получим. Можно заменить на min(1.0, ...).

Да, тогда лучше заменить на min — одно условие, вместо двух у clamp.

Поправил в статье. Однако в шейдере с рамкой один clamp все же нужен, чтобы был цвет рамки/переход/цвет текста.

C чего вы взяли что clamp 0..1 AKA saturate это вообще какие-то инструкции?
Обычно это просто модификатор. 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. И clamp'у все равно, что им обрезают. Но я подозреваю, что в железе clamp может быть реализован без каких-либо ветвлений. Но тут ключевое слово «может быть».
>> Потому, что saturate это не clamp

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.

А чеснок пахнет колбасой. Я правильно уловил ход вашей мысли?
Вам beeruser пытается донести, что saturate ( он же clamp(.., 0,1) ) — будет быстрее min, который вы предложили автору.
Я даже на всякий случай проверил. Код:
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;
MVP — Model*View*Projection матрица. Вам надо гуглить что такое матрица вида, матрица проекции и матрица модели.
float tx=texture2D(tex0, outTexCord).r;

Почему в этой строке участвует именно r-компонента, а не a?

Потому что текстура черно-белая в формате GL_LUMINANCE

Одного меня смущает эта строка?

if(a>=224 && a<=223)return a-32;

(в Бонус! Изменяем реестр unicode символа.)
Sign up to leave a comment.

Articles