-
Автор темы
- #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 и все заработает.
Всем спасибо за прочтение! Кто блять выбирал эти цвета для джава кода на форуме?
Последнее редактирование: