-
Автор темы
- #1
Перед прочтением основного контента ниже, пожалуйста, обратите внимание на обновление внутри секции Майна на нашем форуме. У нас появились:
- бесплатные читы для Майнкрафт — любое использование на свой страх и риск;
- маркетплейс Майнкрафт — абсолютно любая коммерция, связанная с игрой, за исключением продажи читов (аккаунты, предоставления услуг, поиск кодеров читов и так далее);
- приватные читы для Minecraft — в этом разделе только платные хаки для игры, покупайте группу "Продавец" и выставляйте на продажу свой софт;
- обсуждения и гайды — всё тот же раздел с вопросами, но теперь модернизированный: поиск нужных хаков, пати с игроками-читерами и другая полезная информация.
Спасибо!
Предисловие
Всех приветствую! В этой статье я хочу рассказать об одной интересной технологии, которая используется для отрисовки шрифтов в OpenGL. Мне показалось, что она мощнее и лучше обычных растровых шрифтов. Данный способ малоизвестен в кубо-комьюнити, но пришло время это исправить. Происходить все будет внутри игры Minecraft (
Пожалуйста, авторизуйтесь для просмотра ссылки.
в моем случае).Растровые шрифты
Для начала немного поговорим о растровых шрифтах, которые используются большинством для отрисовки текста сейчас.Вкратце разберемся, в чем заключается данная технология: с помощью инструментов из пакета java.awt загружаем шрифт и наносим все нужные символы на картинку, используя наш шрифт. Далее загружаем эту картинку в OpenGL в качестве текстуры. В дальнейшем, указывая различные координаты текстуры, мы можем отрисовать любой символ.
Так выглядит итоговая текстура, хранящая все символы:
Это достаточно простой и топорный способ. У него есть существенный недостаток: символ выглядит хорошо, только если его размер на текстуре в точности соответствует размеру прямоугольника, на который эта текстура натягивается. То есть при скейлинге весь наш красивый антиалиасинг ломается и текст выглядит очень плохо. Поэтому для изменения размера текста приходится повторять все алгоритм для шрифта с другим размером, что приводит к дополнительным затратам на генерацию и хранение каждой новой текстуры.
Шрифты, основанные на "многоканальном поле расстояния со знаком"
MSDF - multichannel signed distance field
Тут то и начинается все веселье, потому что эта технология почти на 100% решает вышеупомянутую проблему, а также открывает некоторые классные возможности. Как минимум, мы повысим четкость шрифта и сможем менять его размер ничего не пересоздавая.MSDF - multichannel signed distance field
Итак, сначала я попытаюсь описать, как это работает, а потом приступим к реализации.
Основой этой технологии является "многоканальное поле расстояния со знаком". Давайте посмотрим, как эти поля выглядят:
На первый взгляд похоже на то, как выглядит окружающий мир в наркотическом трипе, но на самом деле это та же текстура, содержащая все наши символы, только в данном случае эти символы "описаны" не совсем обычно. Для каждого символа было сгенерировано "многоканальное поле расстояния со знаком", мы можем видеть четкие границы между соседними полями.
Многоканальным поле называется потому, что символы описываются 4-мя (RGBA) цветовыми каналами. Как видно, сами символы не имеют четких границ, они будто размыты. Именно благодаря такой неопределенности мы сможем менять размер шрифта без потери качества, правильно рассчитывав границы символов в будущей реализации.
Упрощая, принцип работы можно описать так: есть специальная картинка, которая описывает как примерно должны выглядеть символы. Высчитывать конкретные границы символов мы будем с помощью специального шейдера.
Вот и все. Приступим к самому интересному - к реализации.
Реализация
Для генерации всего необходимого идем на
Пожалуйста, авторизуйтесь для просмотра ссылки.
и скачиваем последний релиз. Автор программы сделал гибкую настройку и хорошо расписал возможные параметры. Читаем и осмысливаем. Обязательные для нас параметры: -type msdf/mtsdf; -format png; -pxrange ставим 10, можно больше или чуть меньше, но разницы не будет; и для указания формата метаданных добавляем -json [filename.json]. Остальное сами допишите. Вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
готовый набор, только поменяйте названия входных и выходных файлов. Кидаем в папку с программой и шрифтом и запускаем батник.У нас появилось два файла font.png и font.json (файлы будут называться так, как вы указали). font.png это наша текстура с символами, а файл font.json содержит метаданные, которые нам очень сильно помогут. Посмотрим на структуру json'а. Сделать это можно например
Пожалуйста, авторизуйтесь для просмотра ссылки.
. В нем содержится информация о шрифте, текстуре, расположении каждого символа на текстуре и его единичные размеры, а также кернинги. Кернинг описывает то, на сколько следует сместить символ 1 по отношению к символу 2. Это своего рода дополнительная настройка отступов, чтобы ваш текст выглядел идеально (
Пожалуйста, авторизуйтесь для просмотра ссылки.
). Реализовывать и учитывать кернинги совсем необязательно.Теперь нам нужно загрузить текстуру и необходимые метаданные. Открываем Eclipse и начинаем писать код (только эклипс!! в интеллидж не получится!).
Текстуру загружаем через обычный майнкрафтовский текстур лоадер. Для загрузки метаданных из json'а воспользуемся возможностями имеющейся библиотеки, а именно вот
Пожалуйста, авторизуйтесь для просмотра ссылки.
методом. Он десериализует json-строку в инстанс класса, в котором мы повторили структуру json'а (ненужные поля отбрасываем).
Java:
public final class IOUtils {
private static final IResourceManager RES_MANAGER = Provider.getResourceManager(); // Minecraft.getResourceManager();
private static final TextureManager TEX_MANAGER = Provider.getTextureManager(); // Minecraft.getTextureManager();
private static final Gson GSON = new Gson();
private IOUtils() {}
public static String toString(ResourceLocation location) {
return toString(location, "\n");
}
public static String toString(ResourceLocation location, String delimiter) {
try (IResource resource = RES_MANAGER.getResource(location);
BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
return reader.lines().collect(Collectors.joining(delimiter));
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
public static <T> T fromJsonToInstance(ResourceLocation location, Class<T> clazz) {
try {
return GSON.fromJson(toString(location), clazz);
} catch (JsonSyntaxException e) {
e.printStackTrace();
return null;
}
}
public static Texture toTexture(ResourceLocation location) {
Texture texture = TEX_MANAGER.getTexture(location);
if (texture == null) {
texture = new SimpleTexture(location);
TEX_MANAGER.register(location, texture);
}
return texture;
}
}
Java:
// the same class structure as in json file generated by https://github.com/Chlumsky/msdf-atlas-gen (01.08.2023). Excluding all unnecessary.
public final class FontData {
private AtlasData atlas;
private MetricsData metrics;
private List<GlyphData> glyphs;
@SerializedName("kerning")
private List<KerningData> kernings;
public AtlasData atlas() {
return this.atlas;
}
public MetricsData metrics() {
return this.metrics;
}
public List<GlyphData> glyphs() {
return this.glyphs;
}
public List<KerningData> kernings() {
return this.kernings;
}
public static final class AtlasData {
@SerializedName("distanceRange")
private float range;
private float width;
private float height;
public float range() {
return this.range;
}
public float width() {
return this.width;
}
public float height() {
return this.height;
}
}
public static final class MetricsData {
private float lineHeight;
private float ascender;
private float descender;
public float lineHeight() {
return this.lineHeight;
}
public float ascender() {
return this.ascender;
}
public float descender() {
return this.descender;
}
public float baselineHeight() {
return this.lineHeight + this.descender;
}
}
public static final class GlyphData {
private int unicode;
private float advance;
private BoundsData planeBounds;
private BoundsData atlasBounds;
public int unicode() {
return this.unicode;
}
public float advance() {
return this.advance;
}
public BoundsData planeBounds() {
return this.planeBounds;
}
public BoundsData atlasBounds() {
return this.atlasBounds;
}
}
public static final class BoundsData {
private float left;
private float top;
private float right;
private float bottom;
public float left() {
return this.left;
}
public float top() {
return this.top;
}
public float right() {
return this.right;
}
public float bottom() {
return this.bottom;
}
}
public static final class KerningData {
@SerializedName("unicode1")
private int leftChar;
@SerializedName("unicode2")
private int rightChar;
private float advance;
public int leftChar() {
return this.leftChar;
}
public int rightChar() {
return this.rightChar;
}
public float advance() {
return this.advance;
}
}
}
Отлично! Как загружать разобрались, теперь все это собираем. Надеюсь не нужно объяснять как пользоваться ResourceLocation и что наши файлы нужно закинуть в ассеты.
Java:
public final class MsdfFont {
private final String name;
private final Texture texture;
private final AtlasData atlas;
private final MetricsData metrics;
private final Map<Integer, MsdfGlyph> glyphs;
private final Map<Integer, Map<Integer, Float>> kernings;
private boolean filtered = false;
private MsdfFont(String name, Texture texture, AtlasData atlas, MetricsData metrics, Map<Integer, MsdfGlyph> glyphs, Map<Integer, Map<Integer, Float>> kernings) {
this.name = name;
this.texture = texture;
this.atlas = atlas;
this.metrics = metrics;
this.glyphs = glyphs;
this.kernings = kernings;
}
public void bind() {
GlStateManager._bindTexture(this.texture.getId());
if (!this.filtered) {
this.texture.setFilter(true, false); // **IMPORTANT**: to correct msdf font anti-aliasing we should set GL_TEXTURE_MAG_FILTER and GL_TEXTURE_MIN_FILTER to GL_LINEAR.
this.filtered = true;
}
}
public void unbind() {
GlStateManager._bindTexture(0);
}
public void applyGlyphs(Matrix4f matrix, IVertexProcessor processor, float size, String text, float thickness, float x, float y, float z, int red, int green, int blue, int alpha) {
int prevChar = -1;
for (int i = 0; i < text.length(); i++) {
int _char = (int) text.charAt(i);
MsdfGlyph glyph = this.glyphs.get(_char);
if (glyph == null)
continue;
Map<Integer, Float> kerning = this.kernings.get(prevChar);
if (kerning != null) {
x += kerning.getOrDefault(_char, 0.0f) * size;
}
x += glyph.apply(matrix, processor, size, x, y, z, red, green, blue, alpha) + thickness;
prevChar = _char;
}
}
public float getWidth(String text, float size) {
int prevChar = -1;
float width = 0.0f;
for (int i = 0; i < text.length(); i++) {
int _char = (int) text.charAt(i);
MsdfGlyph glyph = this.glyphs.get(_char);
if (glyph == null)
continue;
Map<Integer, Float> kerning = this.kernings.get(prevChar);
if (kerning != null) {
width += kerning.getOrDefault(_char, 0.0f) * size;
}
width += glyph.getWidth(size);
prevChar = _char;
}
return width;
}
public String getName() {
return this.name;
}
public AtlasData getAtlas() {
return this.atlas;
}
public MetricsData getMetrics() {
return this.metrics;
}
public static MsdfFont.Builder builder() {
return new Builder();
}
public static class Builder {
public static final String MSDF_PATH = Provider.getFontsPath() + "msdf/";
private String name = "undefined";
private ResourceLocation dataFile;
private ResourceLocation atlasFile;
private Builder() {}
public MsdfFont.Builder withName(String name) {
this.name = name;
return this;
}
// should be json file
public MsdfFont.Builder withData(String dataFile) {
this.dataFile = Provider.getLocation(MSDF_PATH + dataFile);
return this;
}
// should be png file
public MsdfFont.Builder withAtlas(String atlasFile) {
this.atlasFile = Provider.getLocation(MSDF_PATH + atlasFile);
return this;
}
public MsdfFont build() {
FontData data = IOUtils.fromJsonToInstance(this.dataFile, FontData.class); // I LIKE JSON
Texture texture = IOUtils.toTexture(this.atlasFile);
if (data == null)
throw new RuntimeException("Failed to read font data file: " + this.dataFile.toString() +
"; Are you sure this is json file? Try to check the correctness of its syntax.");
float aWidth = data.atlas().width();
float aHeight = data.atlas().height();
Map<Integer, MsdfGlyph> glyphs = data.glyphs().stream()
.collect(Collectors.<GlyphData, Integer, MsdfGlyph>toMap(
(glyphData) -> glyphData.unicode(),
(glyphData) -> new MsdfGlyph(glyphData, aWidth, aHeight)
));
Map<Integer, Map<Integer, Float>> kernings = new HashMap<>();
data.kernings().forEach((kerning) -> {
Map<Integer, Float> map = kernings.get(kerning.leftChar());
if (map == null) {
map = new HashMap<>();
kernings.put(kerning.leftChar(), map);
}
map.put(kerning.rightChar(), kerning.advance());
});
return new MsdfFont(this.name, texture, data.atlas(), data.metrics(), glyphs, kernings);
}
}
}
Java:
public final class MsdfGlyph {
private final int code;
private final float minU, maxU, minV, maxV;
private final float advance, topPosition, width, height;
public MsdfGlyph(GlyphData data, float atlasWidth, float atlasHeight) {
this.code = data.unicode();
this.advance = data.advance();
BoundsData atlasBounds = data.atlasBounds();
if (atlasBounds != null) {
this.minU = atlasBounds.left() / atlasWidth;
this.maxU = atlasBounds.right() / atlasWidth;
this.minV = 1.0F - atlasBounds.top() / atlasHeight;
this.maxV = 1.0F - atlasBounds.bottom() / atlasHeight;
} else {
this.minU = 0.0f;
this.maxU = 0.0f;
this.minV = 0.0f;
this.maxV = 0.0f;
}
BoundsData planeBounds = data.planeBounds();
if (planeBounds != null) {
this.width = planeBounds.right() - planeBounds.left();
this.height = planeBounds.top() - planeBounds.bottom();
this.topPosition = planeBounds.top();
} else {
this.width = 0.0f;
this.height = 0.0f;
this.topPosition = 0.0f;
}
}
public float apply(Matrix4f matrix, IVertexProcessor processor, float size, float x, float y, float z, int red, int green, int blue, int alpha) {
y -= this.topPosition * size;
float width = this.width * size;
float height = this.height * size;
processor.pos(matrix, x, y, z).color(red, green, blue, alpha).tex(this.minU, this.minV).endVertex();
processor.pos(matrix, x, y + height, z).color(red, green, blue, alpha).tex(this.minU, this.maxV).endVertex();
processor.pos(matrix, x + width, y + height, z).color(red, green, blue, alpha).tex(this.maxU, this.maxV).endVertex();
processor.pos(matrix, x + width, y, z).color(red, green, blue, alpha).tex(this.maxU, this.minV).endVertex();
return this.advance * size;
}
public float getWidth(float size) {
return this.advance * size;
}
public int getCharCode() {
return code;
}
}
Обратите внимание, очень важно установить GL_TEXTURE_MAG_FILTER и GL_TEXTURE_MIN_FILTER на GL_LINEAR для нашей текстуры, иначе антиалиасинг не будет работать. Лучшей идеи, чем реализовать кернинги через вложенные мапы, у меня не появилось. Если вы придумали более рациональный подход - напишите.
Последняя важнейшая деталь - шейдер. Работает он на чистой математике. Из названий юниформов должно быть понятно, за что они отвечают. Ниже будет пример. Отмечу только, что "Range" это тот самый -pxrange, который мы указывали при генерации текстуры. Также есть возможность регулировать толщину текста и сразу добавить аутлайн.
C-like:
#version 120
// thanks https://github.com/Blatko1/awesome-msdf & LabyMod
uniform sampler2D Sampler;
uniform vec2 TextureSize;
uniform float Range; // distance field range of the msdf font texture
uniform float EdgeStrength;
uniform float Thickness;
uniform bool Outline; // if false, outline computation will be ignored (and its uniforms)
uniform float OutlineThickness;
uniform vec4 OutlineColor;
in vec4 OutColor;
in vec2 TexCoord;
float median(float red, float green, float blue) {
return max(min(red, green), min(max(red, green), blue));
}
void main() {
vec3 texColor = texture2D(Sampler, TexCoord).rgb;
float dx = dFdx(TexCoord.x) * TextureSize.x;
float dy = dFdy(TexCoord.y) * TextureSize.y;
float toPixels = Range * inversesqrt(dx * dx + dy * dy);
float sigDist = median(texColor.r, texColor.g, texColor.b) - 0.5 + Thickness;
float alpha = smoothstep(-EdgeStrength, EdgeStrength, sigDist * toPixels);
if (Outline) {
float outlineAlpha = smoothstep(-EdgeStrength, EdgeStrength, (sigDist + OutlineThickness) * toPixels) - alpha;
float finalAlpha = alpha * OutColor.a + outlineAlpha * OutlineColor.a;
gl_FragColor = vec4(mix(OutlineColor.rgb, OutColor.rgb, alpha), finalAlpha);
return;
}
gl_FragColor = vec4(OutColor.rgb, OutColor.a * alpha);
}
Java:
public void draw() {
AtlasData atlas = this.font.getAtlas();
this.shader.storeUniform("Sampler", 0);
this.shader.storeUniform("EdgeStrength", 0.5f);
this.shader.storeUniform("TextureSize", atlas.width(), atlas.height());
this.shader.storeUniform("Range", atlas.range());
this.shader.storeUniform("Thickness", this.thickness);
this.shader.storeUniform("Outline", this.outline); // boolean
this.shader.storeUniform("OutlineThickness", this.outlineThickness);
float[] oColor = ColorUtils.normalize(this.outlineColor);
this.shader.storeUniform("OutlineColor", oColor[0], oColor[1], oColor[2], oColor[3]);
int[] color = ColorUtils.unpack(this.color);
this.format.setupRenderState(); // default blending, no culling, no alpha test
this.font.bind();
this.core.begin(this.mode, this.shader); // BufferBuilder.begin(QUADS, POSITION_COLOR_TEX);
this.font.applyGlyphs(matrix, core, size, text, (thickness + outlineThickness * 0.5f) * 0.5f * size,
x, y + font.getMetrics().baselineHeight() * size, z, color[0], color[1], color[2], color[3]);
this.core.draw(); // Tessellator.draw();
this.font.unbind();
this.format.clearRenderState();
this.reset();
}
Java:
public static final MsdfFont DEFAULT = MsdfFont.builder().withName("Biko").withAtlas("font.png").withData("font.json").build();
private void onRender(RenderGameOverlayEvent.Post event) {
if (event.getType() == ElementType.ALL) {
MatrixStack stack = event.getMatrixStack();
Drawer.text().matrix(stack.last().pose()).pos(150, 80).font(DEFAULT).text("MSDF font rendering test!").color(Color.WHITE).size(10).draw();
Drawer.text().matrix(stack.last().pose()).pos(150, 100).font(DEFAULT).thickness(0.2f).text("MSDF font rendering test!").color(Color.WHITE).size(15).draw();
Drawer.text().matrix(stack.last().pose()).pos(150, 130).font(DEFAULT).text("MSDF font rendering test!").outline(0.4f, Color.BLACK).color(Color.WHITE).size(25).draw();
}
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
(качество плохое только на картинке)Теперь я разочарую некоторых из вас: готовый код, который можно скопировать и вставить, я дать не могу. Дело в том, что эта система является частью другого большого проекта. Остальной код я раскрыть не могу, а переписывать отдельно не хочу. По этой же причине я не могу просто загрузить все на гит. Чтобы у вас все заработало, вам придется изменить шейдер, так как тот, который дал я, работает с вертекс шейдером, который основан на атрибутах вершин.
Замените IVertexProcessor на BufferBuilder, комментарии по остальному я там добавил. По идее все должно быть интуитивно понятно.
При использовании слишком жирного текста или аутлайна, у вас могут возникнуть баги. В таком случае вам нужно перегенерировать вашу текстуру с символами с бОльшим -pxrange и все заработает.
Всем спасибо за прочтение! Кто блять выбирал эти цвета для джава кода на форуме?
Последнее редактирование: