Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import meteordevelopment.meteorclient.renderer.MeshBuilder;
import meteordevelopment.meteorclient.renderer.MeshRenderer;
import meteordevelopment.meteorclient.renderer.MeteorRenderPipelines;
import meteordevelopment.meteorclient.renderer.Texture;
import meteordevelopment.meteorclient.utils.Utils;
import meteordevelopment.meteorclient.utils.render.color.Color;
import net.minecraft.client.MinecraftClient;
Expand Down Expand Up @@ -38,7 +39,7 @@ public CustomTextRenderer(FontFace fontFace) {

fonts = new Font[5];
for (int i = 0; i < fonts.length; i++) {
fonts[i] = new Font(buffer, (int) Math.round(27 * ((i * 0.5) + 1)));
fonts[i] = new Font(fontFace, buffer, (int) Math.round(27 * ((i * 0.5) + 1)));
}
}

Expand Down Expand Up @@ -125,11 +126,13 @@ public void end() {
if (!scaleOnly) {
mesh.end();

Texture fontAtlas = font.getTexture();

MeshRenderer.begin()
.attachments(MinecraftClient.getInstance().getFramebuffer())
.pipeline(MeteorRenderPipelines.UI_TEXT)
.mesh(mesh)
.sampler("u_Texture", font.texture.getGlTextureView(), font.texture.getSampler())
.sampler("u_Texture", fontAtlas.getGlTextureView(), fontAtlas.getSampler())
.end();
}

Expand All @@ -139,7 +142,7 @@ public void end() {

public void destroy() {
for (Font font : this.fonts) {
font.texture.close();
font.close();
}
}
}
240 changes: 166 additions & 74 deletions src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,109 +5,186 @@

package meteordevelopment.meteorclient.renderer.text;

import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import com.mojang.blaze3d.textures.TextureFormat;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.*;
import meteordevelopment.meteorclient.renderer.MeshBuilder;
import meteordevelopment.meteorclient.renderer.Texture;
import meteordevelopment.meteorclient.utils.Utils;
import meteordevelopment.meteorclient.utils.render.color.Color;
import net.minecraft.client.texture.NativeImage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.*;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;

import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

public class Font {
public final Texture texture;
private static final int size = 2048;
private final Texture texture;
private final FontFace fontFace;
private final int height;
private final float scale;
private final float ascent;
private final Int2ObjectOpenHashMap<CharData> charMap = new Int2ObjectOpenHashMap<>();
private static final int size = 2048;

public Font(ByteBuffer buffer, int height) {
private final STBTTPackContext packContext;
private final NativeImage fontAtlas;
private @Nullable ByteBuffer fileBuffer;
private WeakReference<ByteBuffer> fileBufferRef;

public Font(FontFace fontFace, @NotNull ByteBuffer buffer, int height) {
this.fontFace = fontFace;
this.fileBuffer = buffer;
this.fileBufferRef = new WeakReference<>(buffer);
this.height = height;

// Initialize font
// allocate data
STBTTFontinfo fontInfo = STBTTFontinfo.create();
STBTruetype.stbtt_InitFont(fontInfo, buffer);

// Allocate buffers
ByteBuffer bitmap = BufferUtils.createByteBuffer(size * size);
STBTTPackedchar.Buffer[] cdata = {
STBTTPackedchar.create(95), // Basic Latin
STBTTPackedchar.create(96), // Latin 1 Supplement
STBTTPackedchar.create(128), // Latin Extended-A
STBTTPackedchar.create(144), // Greek and Coptic
STBTTPackedchar.create(256), // Cyrillic
STBTTPackedchar.create(1) // infinity symbol
};

// create and initialise packing context
STBTTPackContext packContext = STBTTPackContext.create();
STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1);

// create the pack range, populate with the specific packing ranges
STBTTPackRange.Buffer packRange = STBTTPackRange.create(cdata.length);
packRange.put(STBTTPackRange.create().set(height, 32, null, 95, cdata[0], (byte) 2, (byte) 2));
packRange.put(STBTTPackRange.create().set(height, 160, null, 96, cdata[1], (byte) 2, (byte) 2));
packRange.put(STBTTPackRange.create().set(height, 256, null, 128, cdata[2], (byte) 2, (byte) 2));
packRange.put(STBTTPackRange.create().set(height, 880, null, 144, cdata[3], (byte) 2, (byte) 2));
packRange.put(STBTTPackRange.create().set(height, 1024, null, 256, cdata[4], (byte) 2, (byte) 2));
packRange.put(STBTTPackRange.create().set(height, 8734, null, 1, cdata[5], (byte) 2, (byte) 2)); // lol
packRange.flip();

// write and finish
STBTruetype.stbtt_PackFontRanges(packContext, buffer, 0, packRange);
STBTruetype.stbtt_PackEnd(packContext);

// Create texture object and get font scale
texture = new Texture(size, size, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR);
texture.upload(bitmap);
scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height);

// initialize font info & zero out texture
fontAtlas = new NativeImage(NativeImage.Format.LUMINANCE, size, size, false);

packContext = STBTTPackContext.create();
STBTruetype.nstbtt_PackBegin(packContext.address(), fontAtlas.imageId(), size, size, 0, 1, MemoryUtil.NULL);

// Get font vertical ascent
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer ascent = stack.mallocInt(1);
STBTruetype.stbtt_GetFontVMetrics(fontInfo, ascent, null, null);
this.ascent = ascent.get(0);
}
}

public Font(FontFace fontFace, int height) {
this(fontFace, readFont(fontFace), height);
}

for (int i = 0; i < cdata.length; i++) {
STBTTPackedchar.Buffer cbuf = cdata[i];
int offset = packRange.get(i).first_unicode_codepoint_in_range();

for (int j = 0; j < cbuf.capacity(); j++) {
STBTTPackedchar packedChar = cbuf.get(j);

float ipw = 1f / size; // pixel width and height
float iph = 1f / size;

charMap.put(j + offset, new CharData(
packedChar.xoff(),
packedChar.yoff(),
packedChar.xoff2(),
packedChar.yoff2(),
packedChar.x0() * ipw,
packedChar.y0() * iph,
packedChar.x1() * ipw,
packedChar.y1() * iph,
packedChar.xadvance()
));
private void upload() {
RenderSystem.getDevice().createCommandEncoder().writeToTexture(this.texture.getGlTexture(), this.fontAtlas);
}

private ByteBuffer getFileBuffer() {
if (this.fileBuffer == null) {
if (this.fileBufferRef.get() == null) {
ByteBuffer buffer = readFont(this.fontFace);
this.fileBufferRef = new WeakReference<>(buffer);
return this.fileBuffer = buffer;
} else {
return this.fileBuffer = this.fileBufferRef.get();
}
} else {
return this.fileBuffer;
}
}

private static ByteBuffer readFont(FontFace fontFace) {
byte[] data = Utils.readBytes(fontFace.toStream());
return BufferUtils.createByteBuffer(data.length).put(data).flip();
}

// currently unused, but useful in the future hopefully
private void regenerateAtlas() {
ByteBuffer fileBuffer = this.getFileBuffer();

// Allocate buffers
int chars = charMap.size();

STBTTPackRange.Buffer packRange = STBTTPackRange.create(1);
STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(chars);

// create the pack range, populate with the specific packing ranges
IntBuffer charBuffer = BufferUtils.createIntBuffer(chars);
for (int c : charMap.keySet()) charBuffer.put(c);
charMap.clear();
charBuffer.flip();

packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, chars, packedCharBuffer, (byte) 0, (byte) 0));
packRange.flip();

// create and initialise packing context

// pack and upload
STBTruetype.stbtt_PackFontRanges(packContext, fileBuffer, 0, packRange);

this.upload();

// update char data
for (int i = 0; i < chars; i++) {
int codepoint = charBuffer.get(i);
STBTTPackedchar packedChar = packedCharBuffer.get(i);

float ipw = 1f / size; // pixel width and height
float iph = 1f / size;

charMap.put(codepoint, new CharData(
packedChar.xoff(),
packedChar.yoff(),
packedChar.xoff2(),
packedChar.yoff2(),
packedChar.x0() * ipw,
packedChar.y0() * iph,
packedChar.x1() * ipw,
packedChar.y1() * iph,
packedChar.xadvance()
));
}

// clear
this.fileBuffer = null;
}

private CharData getCharData(int codepoint) {
return charMap.computeIfAbsent(codepoint, c -> {
ByteBuffer fileBuffer = this.getFileBuffer();

// allocate buffers
STBTTPackRange.Buffer packRange = STBTTPackRange.create(1);
STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(1);

packRange.put(STBTTPackRange.create().set(height, codepoint, null, 1, packedCharBuffer, (byte) 0, (byte) 0));
packRange.flip();

// pack and upload
STBTruetype.stbtt_PackFontRanges(packContext, fileBuffer, 0, packRange);

// update char data
STBTTPackedchar packedChar = packedCharBuffer.get(0);

float ipw = 1f / size; // pixel width and height
float iph = 1f / size;

return new CharData(
packedChar.xoff(),
packedChar.yoff(),
packedChar.xoff2(),
packedChar.yoff2(),
packedChar.x0() * ipw,
packedChar.y0() * iph,
packedChar.x1() * ipw,
packedChar.y1() * iph,
packedChar.xadvance()
);
});
}

public double getWidth(String string, int length) {
double width = 0;

for (int i = 0; i < length; i++) {
int cp = string.charAt(i);
CharData c = charMap.get(cp);
if (c == null) c = charMap.get(32);

width += c.xAdvance;
for (int i = 0; i < length;) {
int cp = string.codePointAt(i);
CharData c = this.getCharData(cp);
width += c.xAdvance();
i += Character.charCount(cp);
}

return width;
Expand All @@ -123,23 +200,38 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color
int length = string.length();
mesh.ensureCapacity(length * 4, length * 6);

for (int i = 0; i < length; i++) {
int cp = string.charAt(i);
CharData c = charMap.get(cp);
if (c == null) c = charMap.get(32);

for (int i = 0; i < length;) {
int cp = string.codePointAt(i);
CharData c = this.getCharData(cp);
mesh.quad(
mesh.vec2(x + c.x0 * scale, y + c.y0 * scale).vec2(c.u0, c.v0).color(color).next(),
mesh.vec2(x + c.x0 * scale, y + c.y1 * scale).vec2(c.u0, c.v1).color(color).next(),
mesh.vec2(x + c.x1 * scale, y + c.y1 * scale).vec2(c.u1, c.v1).color(color).next(),
mesh.vec2(x + c.x1 * scale, y + c.y0 * scale).vec2(c.u1, c.v0).color(color).next()
mesh.vec2(x + c.x0() * scale, y + c.y0() * scale).vec2(c.u0(), c.v0()).color(color).next(),
mesh.vec2(x + c.x0() * scale, y + c.y1() * scale).vec2(c.u0(), c.v1()).color(color).next(),
mesh.vec2(x + c.x1() * scale, y + c.y1() * scale).vec2(c.u1(), c.v1()).color(color).next(),
mesh.vec2(x + c.x1() * scale, y + c.y0() * scale).vec2(c.u1(), c.v0()).color(color).next()
);

x += c.xAdvance * scale;
x += c.xAdvance() * scale;
i += Character.charCount(cp);
}

return x;
}

public Texture getTexture() {
// flush updates
if (this.fileBuffer != null) {
this.upload();
this.fileBuffer = null;
}

return this.texture;
}

public void close() {
this.texture.close();
this.fontAtlas.close();
STBTruetype.stbtt_PackEnd(this.packContext);
}

private record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
import net.minecraft.util.Identifier;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.lwjgl.BufferUtils;

import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
Expand Down Expand Up @@ -85,11 +83,13 @@ public void end() {
FontHolder fontHolder = it.next();

if (fontHolder.visited) {
Texture fontAtlas = fontHolder.font.getTexture();

MeshRenderer.begin()
.attachments(mc.getFramebuffer())
.pipeline(MeteorRenderPipelines.UI_TEXT)
.mesh(fontHolder.getMesh())
.sampler("u_Texture", fontHolder.font.texture.getGlTextureView(), fontHolder.font.texture.getSampler())
.sampler("u_Texture", fontAtlas.getGlTextureView(), fontAtlas.getSampler())
.end();
}
else {
Expand Down Expand Up @@ -298,10 +298,7 @@ private void onCustomFontChanged(CustomFontChangedEvent event) {
}

private static FontHolder loadFont(int height) {
byte[] data = Utils.readBytes(Fonts.RENDERER.fontFace.toStream());
ByteBuffer buffer = BufferUtils.createByteBuffer(data.length).put(data).flip();

return new FontHolder(new Font(buffer, height));
return new FontHolder(new Font(Fonts.RENDERER.fontFace, height));
}

private static class FontHolder {
Expand All @@ -321,7 +318,7 @@ public MeshBuilder getMesh() {
}

public void destroy() {
font.texture.close();
font.close();
}
}
}