render font glyphs at device resolution to fix pixelation

glyphs were rasterized at gui-space size and then magnified by the
gui scale with nearest-neighbour sampling, making every custom font
blocky. rasters are now built at font-size * gui-scale (one texel per
screen pixel) and drawn through a 1/scale pose, so text is 1:1 and
antialiased at any gui scale. measurements convert back to gui units
and a gui scale change re-rasterizes everything
This commit is contained in:
froslanz
2026-10-22 11:53:39 -04:00
parent b682961856
commit 1f69a3ecb4
+37 -11
View File
@@ -82,6 +82,7 @@ public final class FrosFont {
private static FontMetrics metrics;
private static String loadedName;
private static float loadedSize = Float.NaN;
private static int loadedScale = 1;
private static long lastAttempt;
private static int glyphCounter;
@@ -92,7 +93,7 @@ public final class FrosFont {
if (text == null || text.isEmpty()) return 0;
sync();
if (awtFont == null) return mc.font.width(text);
return metrics.stringWidth(stripCodes(text));
return toGui(metrics.stringWidth(stripCodes(text)));
}
public static int width(Component component) {
@@ -104,7 +105,20 @@ public final class FrosFont {
public static int lineHeight() {
sync();
return awtFont == null ? mc.font.lineHeight : metrics.getHeight();
return awtFont == null ? mc.font.lineHeight : toGui(metrics.getHeight());
}
/** converts a raster pixel measurement to gui units */
private static int toGui(int pixels) {
return (pixels + loadedScale - 1) / loadedScale;
}
private static int currentScale() {
try {
return Math.max(1, (int) Math.round(mc.getWindow().getGuiScale()));
} catch (Exception e) {
return 1;
}
}
public static void draw(GuiGraphics context, String text, int x, int y, int color) {
@@ -140,20 +154,22 @@ public final class FrosFont {
String name = gui == null || vanilla ? "" : gui.font.getValue();
int size = gui == null ? 10 : gui.fontSize.getValue();
if (name == null) name = "";
int scale = currentScale();
boolean changed = !Objects.equals(name, loadedName) || size != loadedSize;
boolean changed = !Objects.equals(name, loadedName) || size != loadedSize || scale != loadedScale;
if (!changed) {
boolean missing = awtFont == null && !name.isBlank();
// retry a missing file every couple of seconds so dropping one
// into the folder loads without touching the setting again
if (!missing || System.currentTimeMillis() - lastAttempt < 2000) return;
}
reset(name, size, changed);
reset(name, size, scale, changed);
}
private static void reset(String name, float size, boolean verbose) {
private static void reset(String name, float size, int scale, boolean verbose) {
loadedName = name;
loadedSize = size;
loadedScale = scale;
lastAttempt = System.currentTimeMillis();
awtFont = null;
metrics = null;
@@ -170,13 +186,13 @@ public final class FrosFont {
Path file = resolveFontFile(name);
if (file != null) {
applyFont(java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, file.toFile()),
size, "Loaded font " + file.getFileName(), verbose);
size * scale, "Loaded font " + file.getFileName(), verbose);
return;
}
java.awt.Font installed = installedFont(name);
if (installed != null) {
applyFont(installed, size, "Using system font '" + name + "'", verbose);
applyFont(installed, size * scale, "Using system font '" + name + "'", verbose);
return;
}
@@ -279,18 +295,28 @@ public final class FrosFont {
if (glyph == null) return;
GLYPHS.put(key, glyph);
}
// the bitmap is rasterized at device resolution (one texel per screen
// pixel); scale the pose down into gui space so the gpu never has to
// magnify it with nearest-neighbour sampling
float scale = loadedScale;
context.pose().pushMatrix();
context.pose().scale(1f / scale, 1f / scale);
context.pose().translate((x - 1) * scale, (y - 1) * scale);
context.blit(RenderPipelines.GUI_TEXTURED, glyph.id(),
x - 1, y - 1, 0.0f, 0.0f, glyph.width(), glyph.height(), glyph.width(), glyph.height());
0, 0, 0.0f, 0.0f, glyph.width(), glyph.height(), glyph.width(), glyph.height());
context.pose().popMatrix();
}
private static Glyph createGlyph(String text, int baseColor) {
String plain = stripCodes(text);
if (plain.isEmpty()) return null;
int pad = loadedScale;
int width = Math.max(1, metrics.stringWidth(plain));
int height = metrics.getHeight();
int imageWidth = width + 2;
int imageHeight = height + 2;
int imageWidth = width + 2 * pad;
int imageHeight = height + 2 * pad;
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
@@ -319,7 +345,7 @@ public final class FrosFont {
}
private static void drawSegments(Graphics2D graphics, String text, int baseColor) {
int baseline = metrics.getAscent() + 1;
int baseline = metrics.getAscent() + loadedScale;
int cursor = 1;
int current = baseColor;
int segmentStart = 0;