LINUX.ORG.RU

PF2: Метрики шрифта

 , , ,


0

1

Пытаюсь сделать самодельный рендеринг текста с помощью PF2 шрифтов и OpenGL. В качестве шрифта использую Droid Sans. Для начала рендерю его в «моноширинном» режиме (поддержку переменной ширины глифа добавлю потом) - просто создаю текстуру, где каждому глифу отводится область maxCharWidth * maxCharHeight, а при рендеринге соответственно на каждую плоскость соответствующую очередному символу натягиваю текстуру по нужным координатам. Проблема в том, что глифы в шрифте могут иметь размеры меньше максимальных и их в этом случае надо правильно позиционировать внутри их места, иначе получается вот так (строчные буквы ниже заглавных и попадают не туда):

https://freeimage.host/i/iI0Yxt

Алгоритм помещения очередного глифа в текстуру шрифта выглядит так:

struct PF2CharHeader {
	uint16_t width;
	uint16_t height;
	int16_t xOffset;
	int16_t yOffset;
	int16_t deviceWidth;
};

...

class PF2FontLoader {
	...
	int m_pointSize;
	int m_maxCharWidth;
	int m_maxCharHeight;
	int m_ascent;
	int m_descent;
	...
}

...

void PF2FontLoader::parseCharBitmap(int index, const PF2CharHeader &header) {
	int x0 = (index % m_colCount) * m_maxCharWidth; //+ header.xOffset;
	int y0 = (index / m_colCount) * m_maxCharHeight; //+ header.yOffset;
	auto base = m_deserializer.adapter().currentReadPos();
	for (int y = 0; y < header.height; y++) {
		int j = (y0 + y) * m_textureWidth + x0;
		for (int x = 0; x < header.width; x++) {
			int i = y * header.width + x;
			uint8_t byte = m_data[base + i / 8];
			if ((byte & (1 << (7 - i % 8))) != 0) {
				m_textureData[j + x] = {255, 255, 255, 255};
			}
		}
	}
}

Также у меня есть параметры m_pointSize, m_ascent и m_descent извлеченные из заголовка шрифта (см. полное описание формата - http://grub.gibibit.com/New_font_format).

По идее ими надо как-то воспользоваться, чтобы вычислить baseline и разместить глиф, когда его размер меньше максимального, однако использовать xOffset и yOffset не помогает. Во-первых, у «e» и «H» одинаковый нулевой yOffset (хотя буквы имеют разную высоту глифа), во-вторых, тогда некоторые другие глифы выходят за границы своего места в текстуре.

Также вот параметры шрифта Droid Sans:

PF2 font "assets/fonts/DroidSans-32.pf2"
PF2 font name: Droid Sans Regular 32
PF2 font family: Droid Sans
PF2 font weight: normal
PF2 font slant: normal
PF2 font point size: 32
PF2 font max char width: 36
PF2 font max char height: 36
PF2 font ascent: 34
PF2 font descent: 9
PF2 font character index has 873 Unicode code point(s) (873 unique glyph(s))
★★★★★

Последнее исправление: KivApple (всего исправлений: 1)

Я в PF2 не разбираюсь, но навереное при таком виде должна быть какаято offset_baseline что-бы это получить и подвинуть pixbuf букафки.

Если ничего подобного в принципе нету и перед рендерингом ты получаешь pixbuf символов и потом уже их используешь, то может написать функцию которая берёт символ и читает картинку с низу вверх до тех пор пока не встретит другое значение оттенка, значит там начинается низ букафки и то столко ты строк прошёл вверх и будет baseline_offset, грузим шрифт, перегоняем нудные символы в карту или по отдельности в полученных pixbuf букавок проводим поиск baseline_offset как выше описал для каждого символа, сохраняем как параметр символа, передаём смещения уже при отрисовке в opengl.

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)

Сделал всё же не «моноширинный» режим:

struct FontGlyphInfo {
	glm::vec2 texCoord;
	glm::vec2 texSize;
	glm::vec2 offset;
	glm::vec2 size;
	float width;
};

...

void PF2FontLoader::parseCharBitmap(int index, const PF2CharHeader &header, FontGlyphInfo &glyph) {
	int x0 = (index % m_colCount) * m_maxCharWidth;
	int y0 = (index / m_colCount) * m_maxCharHeight;
	
	glyph.texCoord.x = static_cast<float>(x0) / static_cast<float>(m_textureWidth);
	glyph.texCoord.y = static_cast<float>(y0) / static_cast<float>(m_textureHeight);
	glyph.texSize.x = static_cast<float>(header.width) / static_cast<float>(m_textureWidth);
	glyph.texSize.y = static_cast<float>(header.height) / static_cast<float>(m_textureHeight);
	glyph.offset.x = static_cast<float>(header.xOffset) / static_cast<float>(m_pointSize);
	glyph.offset.y = static_cast<float>(header.yOffset) / static_cast<float>(m_pointSize);
	glyph.size.x = static_cast<float>(header.width) / static_cast<float>(m_pointSize);
	glyph.size.y = static_cast<float>(header.height) / static_cast<float>(m_pointSize);
	glyph.width = static_cast<float>(header.deviceWidth) / static_cast<float>(m_pointSize);
	
	auto base = m_deserializer.adapter().currentReadPos();
	for (int y = 0; y < header.height; y++) {
		int j = (y0 + y) * m_textureWidth + x0;
		for (int x = 0; x < header.width; x++) {
			int i = y * header.width + x;
			uint8_t byte = m_data[base + i / 8];
			if ((byte & (1 << (7 - i % 8))) != 0) {
				m_textureData[j + x] = {255, 255, 255, 255};
			}
		}
	}
}

...

	glm::vec2 p(0.0f);
	for (auto c : text) {
		if (c != '\n') {
			auto &glyph = m_font->glyphInfo(c);
			auto o = glyph.offset;
			auto s = glyph.size;
			auto t1 = glyph.texCoord;
			auto t2 = t1 + glyph.texSize;
			m_vertexData.insert(m_data.end(), {
					{{o.x + p.x, o.y + p.y + s.y}, {t1.x, t1.y}}, // 1
					{{o.x + p.x + s.x, o.y + p.y + s.y}, {t2.x, t1.y}}, // 2
					{{o.x + p.x, o.y + p.y}, {t1.x, t2.y}}, // 3
					
					{{o.x + p.x + s.x, o.y + p.y + s.y}, {t2.x, t1.y}}, // 2
					{{o.x + p.x + s.x, o.y + p.y}, {t2.x, t2.y}}, // 4
					{{o.x + p.x, o.y + p.y}, {t1.x, t2.y}} // 3
			});
			p.x += glyph.width;
		} else {
			p.y -= 1.0f;
			p.x = 0.0f;
		}
	}

Такой подход в сочетании с тем фактом, что в OpenGL координата Y инвентировона дал нормальный рендеринг:

https://freeimage.host/i/iIhtOG

Но получается, что я вообще не использую параметры ascent и descent, нормально ли это или я что-то упускаю?

KivApple ★★★★★
() автор топика
Последнее исправление: KivApple (всего исправлений: 2)

Прочитал описание формата, там все совершенно понятно. Отталкиваться надо от baseline:

  1. Создать функцию printchar(charDataBuf, baseline,x, return nextX) которая рендерит очередную буковку: принимает описание буковки, текущий baseline, x-координату с которой начать; и возвращает x-координату с которой нужно отрисовать следующую буковку. Очевидно, кроме аргументов на вход, printchar будет использовать только параметры конкретной буковки из charDataBuf (width, height, xoffset, yoffset, deviceWidth) как и сам растер; больше ей ничего не нужно.

  2. Сделать функцию для «перевода каретки» которая смещает на одну строку вниз: на вход берет y-координату текущей baseline, на выход выдает y-координату baseline следующей строки. В простейшей имплементации она может тупо добавлять ascent + descent + leading (которые являеются шрифтовыми константами, кроме leading) и возвращать эту сумму. Произвол есть у выбора константы leading; но поскольку указано что буковки могут выходить за границ ascent-а и descent-а, leading должен быть «достаточно большой» чтобы буковки из соседних строк все же не перекрывались; и в то жо время не намного бОльше чем MAXH. В более сложной имплементации она может учитывать максимальную высоту из всех глифов в только-что отрендерированной строке max(ascent+descent,max(height)) + leading; тогда расстояния по вертикали между baseline могут получаться слегка разными для разных соседних строк; зато буковки перекрываться гарантированно не будут.

Не совсем понятно только где понадобится MAXW (в каждой буковки итак содержится достаточно информации, чтобы ТОЧНО определить смещение по х, с котором нужно начинать рисовать следующую за ней). Может быть для каких-то экзотичных ситуаций, если глифы нужно рисовать сверху-вниз (вместо по горизонтали), чтобы определить x-смещение м/у таких «вертикальных строк»….

manul91
()
Последнее исправление: manul91 (всего исправлений: 3)
Ответ на: комментарий от KivApple

Немного упростил функцию генерации плоскостей для отрисовки:

glm::vec2 p0(0.0f);
for (auto c : text) {
	if (c != '\n') {
		auto &glyph = m_font->glyphInfo(c);
		
		auto p1 = p0 + glyph.offset;
		auto p2 = p1 + glyph.size;
		auto t1 = glyph.texCoord;
		auto t2 = t1 + glyph.texSize;
			
		const TextRendererVertexData v[] = {
				{{p1.x, p2.y}, {t1.x, t1.y}},
				{{p2.x, p2.y}, {t2.x, t1.y}},
				{{p1.x, p1.y}, {t1.x, t2.y}},
				{{p2.x, p1.y}, {t2.x, t2.y}}				
		};
		
		m_data.emplace_back(v[0]);
		m_data.emplace_back(v[1]);
		m_data.emplace_back(v[2]);
		
		m_data.emplace_back(v[1]);
		m_data.emplace_back(v[2]);
		m_data.emplace_back(v[3]);
		
		p0.x += glyph.width;
	} else {
		p0.y -= 1.0f;
		p0.x = 0.0f;
	}
}
KivApple ★★★★★
() автор топика
Последнее исправление: KivApple (всего исправлений: 1)
Ответ на: комментарий от manul91

Я рисую текст не руками во фреймбуфер, а сначала генерирую из шрифта вот такую текстуру:

https://freeimage.host/i/iIO5mu

А затем генерирую прямоугольники с заданными текстурными координатами по принципу один прямоугольник - один глиф. А затем через OpenGL отрисовываю это всё на экране.

KivApple ★★★★★
() автор топика
Ответ на: комментарий от KivApple

Так очевидно что в этом случае получится что-то уродливое, если не придерживаться baseline и не учитывать детали смещений и размеров каждого глифа.

Впрочем, и «с прямоугольниками» (они должны быть разными размерами) может получиться правильно, если правильно учитывать их смещения вниз, вверх, и по горизонтали до соседа (для каждого прямоугольника они будут разными).

В чем вообще состоит ваш «вопрос»? У вас есть спецификация, вы отрисовываете не по спецификации.

manul91
()
Последнее исправление: manul91 (всего исправлений: 5)
Ответ на: комментарий от KivApple

Как написал выше, очевидно же что ascent, descent, MAXH, MAXW уместно использовать чтобы более эффективно рассчитывать расстояния между соседних строк (поскольку в вашей терминологии, каждая строка состоит из разных по размеров прямоугольников, смещенных по-разному вверх и вниз).

Как их конкретно использовать оставлено на имплементации («стиля»), сам шрифт этого не диктует.

Например, если нужно чтобы строки были как можно плотнее по вертикали, но при этом глифы никогда не наезжали друг поверх друга - если брать фиксированное вертикальное смещение в пикселях между baseline соседних строк - то оно должно быть больше чем ascent+descent но не меньше чем MAXH+1.

manul91
()
Последнее исправление: manul91 (всего исправлений: 1)
Ответ на: комментарий от BadUser

Мне лень было гуглить про формат вот я и предположил что там нет информации что-бы правильно разместить всё. Вон выше у ТС всё нормально.

Только я бы ещё прифигачил SDF карты (чёт я зачастил про эту штуку говорить) https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf тогда на выходе будет гладкая красота.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Шрифт в формате PF2 уже в низком разрешении. Как я понимаю, для этой техники нужно иметь версию в высоком разрешении, чтобы построить SDF карту, а её в общем случае нет. Это надо загрузку TTF делать, а это значит тащить FreeType, а это совсем другой уровень.

KivApple ★★★★★
() автор топика
Ответ на: комментарий от KivApple

Ну да, только даже с финальной текстурой https://freeimage.host/i/font.iIO5mu если прям её взять и привести к SDF то получится https://i.ibb.co/vqbyTMg/out.png Теперь можно взять и порезать размер изображения раза в 2. При отрисовке шрифта регулируя контраст и яркость https://www.veed.io/view/33f52326-370b-47f8-9971-1fe87e985754 (залил на первое попавшееся, тут в гимпе, а ты в шейдере тож самое делаешь) ты можешь добиться одинакового начертания при любом масштабе символа. В добавок, просто регулируя контрастность сможешь делать обводку белых символов чёрным цветом или любым другим, да хоть радугой SDF даёт падающую градацию каждый уровень которого можно красить во что угодно нутыпонял. Так что даже без создания текстуры в 8к генерации SDF и даунгрейда до ~~512x512~ 2k при сохранении качества финальных букавок можно использовать другие побочки. Даже возможность регулировать «размытие» и легко превращать шрифт в bold стоит того имхо. Уж слишком универсальная штука =)

Если даже твою финальную текстуру шрифта в 2 раза по размеру порезать после SDF при отрисовке яркость пониже/понтраст повыше и разницы нету. Уже память экономится на текстуре =) хотя не, это я маху дал, херня выходит =) Даааа, надо всё на глаз подгонять и всё такое. Один фиг прикольная фигулька =)

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 2)