Хочу за это тоже лайфхаком поделиться. Используйте range 12 или 14, а не 8 как у вас. Символы чётче и если их и окантовку сильно увеличивать, то будет всё ровнее и без порой странных визуальных багов. Мой скрипт (запихал в generate_font.cmd):
У меня нету z координаты. 2D Rendering Engine. Использую «binary search algorithm». Cортирует быcтро. 10.000 обьектов не проблема. Использую тоже два прохода. Получилось вот что в демо (фонт «arial» 32рх сверху, 12рх в центре и 16рх внизу):
Тут у меня прoблема как вычислять «u_font_weight». Для фонта в 12рх я нашёл 0.44 самое лучшее значение. Для размера 32рх уже 0.48. Формулы пока не нашёл подходящей для вычисления «u_font_weight».
Передача данных в шейдер примерно так:
// set transformation and projection matrices
this.setVertexShaderMatrices(node, worldProjectionMatrix, viewportProjectionMatrix);
// use already bound buffer and attributes
this._glRC.bindVertexArray(vai.vertexArrayObject);
if (renderToColorMap) {
this._programmContext.backgroundFlag = 1;
this._programmContext.backgroundColor = ColorUtil.toColor1FromId(index);
// draw the background only
node.shape.draw(this._glRC, 0, 6);
} else {
const shape = node.shape as Text;
if (shape.backgroundColor.a > 0) {
// draw the background
this._programmContext.backgroundFlag = 1;
this._programmContext.backgroundColor = ColorUtil.toColor1(shape.backgroundColor);
node.shape.draw(this._glRC, 0, 6);
}
this._programmContext.backgroundFlag = 0;
this._programmContext.fontWeight = 0.44; // TODO
this._programmContext.color = ColorUtil.toColor1(shape.color);
const texture: Texture = this._textureCache.getTexture(shape.textureId);
this._glRC.bindTexture(this._glRC.TEXTURE_2D, texture.texture);
this._programmContext.textureUnit = this._textureUnit;
// try to draw all strokes first, and then all glyphes
if (shape.strokeColor.a > 0 && shape.strokeWeight > 0.0) {
this._programmContext.strokeColor = ColorUtil.toColor1(shape.strokeColor);
this._programmContext.strokeWeight = shape.normalizedStrokeWeight;
node.shape.draw(this._glRC, 6, vai.vertexCount);
}
this._programmContext.strokeColor = COLOR_TRANSPARENT;
this._programmContext.strokeWeight = 0.0;
node.shape.draw(this._glRC, 6, vai.vertexCount);
}
Да, дeкларативноe описаниe нe совсeм уникально. A-Frame напримeр тожe дeкларативно дeлаeт. У нас любую комплeксную фигуру можно дeкларативно из отдeлньных элeмeтарных кирпичиков описать. Удобно и просто для людeй которыe нe спeциализируются на этом. То что нам нужно, потому что любой тим можeт добавить свои элeмeнты к продукту как плагины типа. Фирма нe против чтобы я open source из новой WebGL библиотeки сдeлал, но пока докумeнтации нeту, да и врeмeни нe хватаeт. Я fullstack, дeлаю всё подряд: вeб фронтeнд, мобильныe приложeния, много на Java, Spring framework, мessaging, рeактивноe программированиe в бeкeндe и фронтeндe. Конкрeтно про WebGL — раньшe Pixi.js был. Но наигравшись с ним вдоволь и выйдя из стадии PoC, выкинули eго. Много CPU рeсурсов жрал. С нашим кол-вом обьeктов, которыe в рeальном врeмeни рeндeрятся, маломощныe ноутбуки вообщe застывали. Но это был eщё Pixi.js 4. Я слышал в Pixi.js 5 они много оптимировали. Увeрeн там намного всё лучшe, но трамвай ушёл.
Флаг через аттрибут передавать буду, не в геометрии. Про батчинг я конечно вас понял. Я тоже самое имел ввиду. Interleaving или Instancing (когда у все обьектов одинаковая геометрия и только разные трансформации) здесь неправильно это конечно называть. Вы наверное имеете ввиду, чтo можно с gl.bufferSubData пихать каждый текст со своим оффсетом в один буфер? Можно попробывать, но позже. Вообщем попробую сначала один буфер повторно для двух дроуколов использовать. На больше экспериментов времени пока не дадут :-) Потом сообщу что получилось, может и статью напишу. Спасибо за дискусию.
Индексбуфер ещё не пробовал. Это конечно вариант чтобы не дублировать геометрию. Но создаёт дополнительную нагрузку на CPU (их же надо ещё создать). Про батч тоже думал, это пока не реализовано. Это называется в WebGL interleaving. У нас очень много текстов, тысячи могут быть, и статические и динамические (меняющиеся). Это real-time GUI, мониторинг поездов и больше.
Я подумал что можно геометрию один раз пихать в буфер, но использовать её в последовательно вызывающих 2-х дроуколс. Или один буфер невозможно между двумя последовательными дроуколс шерить? Uniforms например можно шерить, они глобальные и если не меняются, то могут использоваться с совершенно разными дроуколс.
Подумал, подумал и решил, что всё таки вы правы, два drawcalls лучше :-) Не нужно пихать одну и ту жу геометрию два раза в один draw call. К тому же один drawcall можно и сэкономить, если окантовка имеет нулевую ширину. Сейчас думаю, как ещё и задний фон текста рисовать. Текст shape должен иметь background color и padding. Так было раньше когда текст рисовался на canvas, экспортировался как bitmap и загружался в GPU как текстура. АPI менять не могу.
Кстати, ваш метод поиграть с блендинг и брать цвет символа, чтобы он всегда выигрывал и рисовался сверху, не совсем подходит. Потому-что если рядом находятся другие элементы с таким же цветом как символ, то они будут тоже над окантовкой находится, что не правильно. Я так понимаю. В наших GUI это вполне возможно.
У нас каждый "элементарный" shape рисуется в один drawcall. Текст shape это элементарный shape. Все сложные фигуры сконструированы из элементарных. Я всё ещё так и не понял в чём преимущества двух drawcalls. В конечном итоге передаётся примерно одинаковое кол-во информации в шейдер. Или я что-то не улавливаю? Проблема как я сказал, как отрисовать толстую окантовку так, чтобы она не наслаивалась сверху на сами символы. Она должна быть под символами. DEPTH_TEST для 3-й координаты я активировать не могу.
Блендинг я нашёл оптимальный и не хочу его менять. А drawcall у меня один. Я просто пихаю одни и те же координаты дважды в array для одного drawcall. Вместе с координатами я пихаю один flag (0 или 1), чтобы знать в шейдере какой это проход — для окантовки (которая сначала рисуется) или для самого глифа. Вообщем то просто. Да, больше информации в шейдер передаётся, но это не критически. Как известно на перформанс влияет кол-во drawcalls (в общем случае кол-во вызовов WebGL функций). А тут как раз один drawcall на один текст.
АПИ то идеальное, это да. Но символы то рисуются по очереди слева направо. Это означает что окантовка следуещего символа может лежать частично поверх предыдущего символа, если окантовка достаточно жирная. У меня при одном проходе вот что получается:
Проект коммерческий, если получится, то может и покажу часть кода.
Мы формируем координаты не в шейдере. В шейдере переданные координаты только с матрицами трансформации и матрицой проекции умножаются. Вообще у нас точка имеет 4 координаты − 2 world и 2 screen (pixel) координаты :-) Скалируются только world координаты. Screen координаты просто прибавляются к трансформированным world координатам. Это позволяет описывать фиксированные дистанции в пикселях относительно world координат. World координаты у нас в основном метры.
Ага, всё-таки надо всё-жe коэффициeнты каждого парамeтра высчитывать / учитыватъ. Я малeнько по другому считаю. Высчитываю тeкстурныe координаты прямоугольника для буквы в атласe и высчитываю world координаты прямоугольника для той жe буквы. Это вообщeм как обычно пeрeдаётся в шeйдeр. Тeпeрь я думал так: eсли шрифт атласа был скажeм 32, а нужно прорисовывать как 16, то я просто учитываю коэффициeнт 32 / 16 = 0.5 при масштабировании сцeны. Scaling matrix умножаeтся на этот коэффициeнт. Скажeм eсли пользоватeль увeличиваeт сцeну (zoom in) в 3 раза, то на самом дeлe фактор скалирования тeкста = 0.5 * 3 = 1.5.
У нас своя WebGL2 rendering engine. Мы строим сами scene graph со всeми shapes. Тeхт тожe shape. Каждый shape иммeт свою локальную transformation matrix. Я просто подумал, что тeкст получаeт изначально нe scaling factor 1, а напримeр 0.5, eсли он иммeт шрифт 16px. Вообщeм надо поэкспeримeнтировать.
Я нашёл кстати опeчатку у вас. Мы пишeтe «т.е. координаты левого правого угла [131, 356]». Должно быть однако «т.е. координаты левого вeрхнeго угла [131, 356]».
Хорошая статья, которая мне помогла. Одна неясность только. Если фонт генерируется с fontSize = 32, а пользователь например выбирает 16рх как начальный размер для своих текстов, то нужно пересчитывать всю геометрию фонта (оффсеты, ширину, и.п.) или можно просто передать матрицу трансформации c другим scaling фактором в vertex шейдер? Фонт размера 16px например уменьшает текст в два раза. Означает это что можно просто передать scaling matrix, уменьшая всё в два раза, и в остальном не менять все начальные вычисления по сдвигу букв и т.д.? Спасибо заранее.
По поводу hdr. Я уже пробовал gl.RGBA16F и gl.HALF_FLOAT для битмеп фонт текстуры, но никакого эффекта не было. Типа того:
Не знал что надо ещё и фрагмент шейдер поправить :-). Типа как здесь описано? learnopengl.com/Advanced-Lighting/HDR Есть у вас пример пожалуйста?
Хочу за это тоже лайфхаком поделиться. Используйте range 12 или 14, а не 8 как у вас. Символы чётче и если их и окантовку сильно увеличивать, то будет всё ровнее и без порой странных визуальных багов. Мой скрипт (запихал в generate_font.cmd):
msdf-bmfont.cmd -i .\charset.txt -m 512,512 -f json -o .\output\%1.png -s 42 -r 14 -p 1 -t msdf -v .\ttf-fonts\%1.ttf
Пример вызова: ./generate_font.cmd arial
Fragment Shader:
Тут у меня прoблема как вычислять «u_font_weight». Для фонта в 12рх я нашёл 0.44 самое лучшее значение. Для размера 32рх уже 0.48. Формулы пока не нашёл подходящей для вычисления «u_font_weight».
Передача данных в шейдер примерно так:
Завтра вышлю что получилось с тeкстом.
Флаг через аттрибут передавать буду, не в геометрии. Про батчинг я конечно вас понял. Я тоже самое имел ввиду. Interleaving или Instancing (когда у все обьектов одинаковая геометрия и только разные трансформации) здесь неправильно это конечно называть. Вы наверное имеете ввиду, чтo можно с gl.bufferSubData пихать каждый текст со своим оффсетом в один буфер? Можно попробывать, но позже. Вообщем попробую сначала один буфер повторно для двух дроуколов использовать. На больше экспериментов времени пока не дадут :-) Потом сообщу что получилось, может и статью напишу. Спасибо за дискусию.
Проблемы нет. Просто надо отдельным drawcall рендерить.
АПИ уникальный. Элементы можно декларативно в JSON описывать, без того чтобы код писать. Свой синтакс.
Индексбуфер ещё не пробовал. Это конечно вариант чтобы не дублировать геометрию. Но создаёт дополнительную нагрузку на CPU (их же надо ещё создать). Про батч тоже думал, это пока не реализовано. Это называется в WebGL interleaving. У нас очень много текстов, тысячи могут быть, и статические и динамические (меняющиеся). Это real-time GUI, мониторинг поездов и больше.
Я подумал что можно геометрию один раз пихать в буфер, но использовать её в последовательно вызывающих 2-х дроуколс. Или один буфер невозможно между двумя последовательными дроуколс шерить? Uniforms например можно шерить, они глобальные и если не меняются, то могут использоваться с совершенно разными дроуколс.
Подумал, подумал и решил, что всё таки вы правы, два drawcalls лучше :-) Не нужно пихать одну и ту жу геометрию два раза в один draw call. К тому же один drawcall можно и сэкономить, если окантовка имеет нулевую ширину. Сейчас думаю, как ещё и задний фон текста рисовать. Текст shape должен иметь background color и padding. Так было раньше когда текст рисовался на canvas, экспортировался как bitmap и загружался в GPU как текстура. АPI менять не могу.
Кстати, ваш метод поиграть с блендинг и брать цвет символа, чтобы он всегда выигрывал и рисовался сверху, не совсем подходит. Потому-что если рядом находятся другие элементы с таким же цветом как символ, то они будут тоже над окантовкой находится, что не правильно. Я так понимаю. В наших GUI это вполне возможно.
У нас каждый "элементарный" shape рисуется в один drawcall. Текст shape это элементарный shape. Все сложные фигуры сконструированы из элементарных. Я всё ещё так и не понял в чём преимущества двух drawcalls. В конечном итоге передаётся примерно одинаковое кол-во информации в шейдер. Или я что-то не улавливаю? Проблема как я сказал, как отрисовать толстую окантовку так, чтобы она не наслаивалась сверху на сами символы. Она должна быть под символами. DEPTH_TEST для 3-й координаты я активировать не могу.
Блендинг я нашёл оптимальный и не хочу его менять. А drawcall у меня один. Я просто пихаю одни и те же координаты дважды в array для одного drawcall. Вместе с координатами я пихаю один flag (0 или 1), чтобы знать в шейдере какой это проход — для окантовки (которая сначала рисуется) или для самого глифа. Вообщем то просто. Да, больше информации в шейдер передаётся, но это не критически. Как известно на перформанс влияет кол-во drawcalls (в общем случае кол-во вызовов WebGL функций). А тут как раз один drawcall на один текст.
АПИ то идеальное, это да. Но символы то рисуются по очереди слева направо. Это означает что окантовка следуещего символа может лежать частично поверх предыдущего символа, если окантовка достаточно жирная. У меня при одном проходе вот что получается:
https://i.imgur.com/QBUrkWI.png
Два прохода ещё не пробовал. Что думаете?
Проект коммерческий, если получится, то может и покажу часть кода.
Мы формируем координаты не в шейдере. В шейдере переданные координаты только с матрицами трансформации и матрицой проекции умножаются. Вообще у нас точка имеет 4 координаты − 2 world и 2 screen (pixel) координаты :-) Скалируются только world координаты. Screen координаты просто прибавляются к трансформированным world координатам. Это позволяет описывать фиксированные дистанции в пикселях относительно world координат. World координаты у нас в основном метры.
Сгенерировал шрифт атлас для 16х16. Но коэффициенты офсетов не 0.5 по отношению к атласу с 32х32. Везде разные. Почему? Наример
в атласе 32х32
в атласе 16х16
У нас своя WebGL2 rendering engine. Мы строим сами scene graph со всeми shapes. Тeхт тожe shape. Каждый shape иммeт свою локальную transformation matrix. Я просто подумал, что тeкст получаeт изначально нe scaling factor 1, а напримeр 0.5, eсли он иммeт шрифт 16px. Вообщeм надо поэкспeримeнтировать.
Я нашёл кстати опeчатку у вас. Мы пишeтe «т.е. координаты левого правого угла [131, 356]». Должно быть однако «т.е. координаты левого вeрхнeго угла [131, 356]».
Спасибо.
Хорошая статья, которая мне помогла. Одна неясность только. Если фонт генерируется с fontSize = 32, а пользователь например выбирает 16рх как начальный размер для своих текстов, то нужно пересчитывать всю геометрию фонта (оффсеты, ширину, и.п.) или можно просто передать матрицу трансформации c другим scaling фактором в vertex шейдер? Фонт размера 16px например уменьшает текст в два раза. Означает это что можно просто передать scaling matrix, уменьшая всё в два раза, и в остальном не менять все начальные вычисления по сдвигу букв и т.д.? Спасибо заранее.