Not a hack at all

- Get text instancing minimally working
- Use special glyph instance type to instance all characters under the
  same mesh
- Unfortunately MC's font stuff is not threading friendly so hacks were
  required to allow visuals to query glyphs from their beginFrame
- Mixin to CodePointMap to overwrite most methods and synchronize
- Mixin to FontSet to use a custom AsyncFontTexture that defers texture
  init and upload
- Trying to mixin directly to FontTexture or subclass it is very
  complicated because of the calls to getId, but I think that can be
  improved
This commit is contained in:
Jozufozu 2024-09-24 17:30:16 -07:00
parent 40577420d5
commit 9227631c73
18 changed files with 961 additions and 0 deletions

View file

@ -2,6 +2,8 @@ package dev.engine_room.flywheel.backend.engine;
import java.util.List;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.engine_room.flywheel.api.RenderContext;
import dev.engine_room.flywheel.api.backend.Engine;
import dev.engine_room.flywheel.api.instance.Instance;
@ -90,6 +92,7 @@ public class EngineImpl implements Engine {
@Override
public void setupRender(RenderContext context) {
try (var state = GlStateTracker.getRestoreState()) {
RenderSystem.replayQueue();
Uniforms.update(context);
environmentStorage.flush();
drawManager.flush(lightStorage, environmentStorage);

View file

@ -0,0 +1,159 @@
package dev.engine_room.flywheel.backend.font;
import java.nio.file.Path;
import java.util.List;
import org.apache.commons.compress.utils.Lists;
import org.jetbrains.annotations.Nullable;
import com.mojang.blaze3d.font.SheetGlyphInfo;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.platform.TextureUtil;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.engine_room.flywheel.lib.internal.GlyphExtension;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.font.GlyphRenderTypes;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.client.renderer.texture.Dumpable;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
public class AsyncFontTexture extends AbstractTexture implements Dumpable {
private static final int SIZE = 256;
private final GlyphRenderTypes renderTypes;
private final ResourceLocation name;
private final boolean colored;
private final Node root;
private final List<Upload> uploads = Lists.newArrayList();
private boolean flushScheduled = false;
public AsyncFontTexture(ResourceLocation name, GlyphRenderTypes renderTypes, boolean colored) {
this.name = name;
this.colored = colored;
this.root = new Node(0, 0, 256, 256);
this.renderTypes = renderTypes;
if (RenderSystem.isOnRenderThreadOrInit()) {
this.init();
} else {
RenderSystem.recordRenderCall(this::init);
}
}
@Override
public void load(ResourceManager resourceManager) {
}
@Override
public void close() {
this.releaseId();
}
@Nullable
public BakedGlyph add(SheetGlyphInfo glyphInfo) {
if (glyphInfo.isColored() != this.colored) {
return null;
}
Node node = this.root.insert(glyphInfo);
if (node != null) {
if (RenderSystem.isOnRenderThreadOrInit()) {
this.bind();
glyphInfo.upload(node.x, node.y);
} else {
uploads.add(new Upload(glyphInfo, node.x, node.y));
if (!flushScheduled) {
RenderSystem.recordRenderCall(this::flush);
flushScheduled = true;
}
}
var out = new BakedGlyph(this.renderTypes, ((float) node.x + 0.01f) / 256.0f, ((float) node.x - 0.01f + (float) glyphInfo.getPixelWidth()) / 256.0f, ((float) node.y + 0.01f) / 256.0f, ((float) node.y - 0.01f + (float) glyphInfo.getPixelHeight()) / 256.0f, glyphInfo.getLeft(), glyphInfo.getRight(), glyphInfo.getUp(), glyphInfo.getDown());
((GlyphExtension) out).flywheel$texture(name);
return out;
}
return null;
}
@Override
public void dumpContents(ResourceLocation resourceLocation, Path path) {
String string = resourceLocation.toDebugFileName();
TextureUtil.writeAsPNG(path, string, this.getId(), 0, 256, 256, i -> (i & 0xFF000000) == 0 ? -16777216 : i);
}
public void init() {
TextureUtil.prepareImage(colored ? NativeImage.InternalGlFormat.RGBA : NativeImage.InternalGlFormat.RED, this.getId(), 256, 256);
}
public void flush() {
this.bind();
for (Upload upload : this.uploads) {
upload.info.upload(upload.x, upload.y);
}
uploads.clear();
flushScheduled = false;
}
public record Upload(SheetGlyphInfo info, int x, int y) {
}
@Environment(value = EnvType.CLIENT)
static class Node {
final int x;
final int y;
private final int width;
private final int height;
@Nullable
private Node left;
@Nullable
private Node right;
private boolean occupied;
Node(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Nullable Node insert(SheetGlyphInfo glyphInfo) {
if (this.left != null && this.right != null) {
Node node = this.left.insert(glyphInfo);
if (node == null) {
node = this.right.insert(glyphInfo);
}
return node;
}
if (this.occupied) {
return null;
}
int i = glyphInfo.getPixelWidth();
int j = glyphInfo.getPixelHeight();
if (i > this.width || j > this.height) {
return null;
}
if (i == this.width && j == this.height) {
this.occupied = true;
return this;
}
int k = this.width - i;
int l = this.height - j;
if (k > l) {
this.left = new Node(this.x, this.y, i, this.height);
this.right = new Node(this.x + i + 1, this.y, this.width - i - 1, this.height);
} else {
this.left = new Node(this.x, this.y, this.width, j);
this.right = new Node(this.x, this.y + j + 1, this.width, this.height - j - 1);
}
return this.left.insert(glyphInfo);
}
}
}

View file

@ -0,0 +1,146 @@
package dev.engine_room.flywheel.backend.mixin;
import java.util.Arrays;
import java.util.function.IntFunction;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import net.minecraft.client.gui.font.CodepointMap;
@Mixin(CodepointMap.class)
public class CodePointMapMixin<T> {
@Shadow
@Final
private T[][] blockMap;
@Shadow
@Final
private T[] empty;
@Shadow
@Final
private IntFunction<T[]> blockConstructor;
@Unique
private final Object flywheel$lock = new Object();
/**
* @author
* @reason
*/
@Overwrite
public void clear() {
synchronized (flywheel$lock) {
Arrays.fill(this.blockMap, this.empty);
}
}
/**
* @author
* @reason
*/
@Nullable
@Overwrite
public T get(int index) {
int i = index >> 8;
int j = index & 0xFF;
synchronized (flywheel$lock) {
return this.blockMap[i][j];
}
}
/**
* @author
* @reason
*/
@Nullable
@Overwrite
public T put(int index, T value) {
int i = index >> 8;
int j = index & 0xFF;
T object;
synchronized (flywheel$lock) {
T[] objects = this.blockMap[i];
if (objects == this.empty) {
objects = this.blockConstructor.apply(256);
this.blockMap[i] = objects;
objects[j] = value;
return null;
}
object = objects[j];
objects[j] = value;
}
return object;
}
/**
* @author
* @reason
*/
@Overwrite
public T computeIfAbsent(int index, IntFunction<T> valueIfAbsentGetter) {
int i = index >> 8;
int j = index & 0xFF;
T out;
synchronized (flywheel$lock) {
T[] objects = this.blockMap[i];
T object = objects[j];
if (object != null) {
return object;
}
if (objects == this.empty) {
objects = this.blockConstructor.apply(256);
this.blockMap[i] = objects;
}
out = valueIfAbsentGetter.apply(index);
objects[j] = out;
}
return out;
}
/**
* @author
* @reason
*/
@Nullable
@Overwrite
public T remove(int index) {
int i = index >> 8;
int j = index & 0xFF;
T object;
synchronized (flywheel$lock) {
T[] objects = this.blockMap[i];
if (objects == this.empty) {
return null;
}
object = objects[j];
objects[j] = null;
}
return object;
}
/**
* @author
* @reason
*/
@Overwrite
public void forEach(CodepointMap.Output<T> output) {
synchronized (flywheel$lock) {
for (int i = 0; i < this.blockMap.length; ++i) {
T[] objects = this.blockMap[i];
if (objects == this.empty) continue;
for (int j = 0; j < objects.length; ++j) {
T object = objects[j];
if (object == null) continue;
int k = i << 8 | j;
output.accept(k, object);
}
}
}
}
}

View file

@ -0,0 +1,121 @@
package dev.engine_room.flywheel.backend.mixin;
import java.util.List;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.google.common.collect.Lists;
import com.mojang.blaze3d.font.GlyphInfo;
import com.mojang.blaze3d.font.GlyphProvider;
import com.mojang.blaze3d.font.SheetGlyphInfo;
import dev.engine_room.flywheel.backend.font.AsyncFontTexture;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.GlyphRenderTypes;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
@Mixin(FontSet.class)
public abstract class FontSetMixin {
@Shadow
@Final
private TextureManager textureManager;
@Shadow
private BakedGlyph missingGlyph;
@Shadow
@Final
private ResourceLocation name;
@Shadow
@Final
private Int2ObjectMap<IntList> glyphsByWidth;
@Shadow
public abstract BakedGlyph getGlyph(int character);
@Unique
private static final RandomSource RANDOM = RandomSource.createNewThreadLocalInstance();
@Unique
private List<AsyncFontTexture> flywheel$textures;
@Inject(method = "<init>", at = @At("TAIL"))
public void init(TextureManager textureManager, ResourceLocation name, CallbackInfo ci) {
flywheel$textures = Lists.newArrayList();
}
@Inject(method = "reload", at = @At("TAIL"))
public void reload(List<GlyphProvider> glyphProviders, CallbackInfo ci) {
flywheel$closeTextures();
}
/**
* @author Jozufozu
* @reason Use thread safe random
*/
@Overwrite
public BakedGlyph getRandomGlyph(GlyphInfo glyph) {
IntList intList = this.glyphsByWidth.get(Mth.ceil(glyph.getAdvance(false)));
if (intList != null && !intList.isEmpty()) {
// Override to use thread safe random
// FIXME: can we just replace the static field instead?
return this.getGlyph(intList.getInt(RANDOM.nextInt(intList.size())));
}
return this.missingGlyph;
}
/**
* @author Jozufozu
* @reason Use our stitching
*/
@Overwrite
private BakedGlyph stitch(SheetGlyphInfo glyphInfo) {
for (AsyncFontTexture fontTexture : flywheel$textures) {
BakedGlyph bakedGlyph = fontTexture.add(glyphInfo);
if (bakedGlyph == null) continue;
return bakedGlyph;
}
ResourceLocation resourceLocation = this.name.withSuffix("/" + flywheel$textures.size());
boolean bl = glyphInfo.isColored();
GlyphRenderTypes glyphRenderTypes = bl ? GlyphRenderTypes.createForColorTexture(resourceLocation) : GlyphRenderTypes.createForIntensityTexture(resourceLocation);
AsyncFontTexture fontTexture2 = new AsyncFontTexture(resourceLocation, glyphRenderTypes, bl);
flywheel$textures.add(fontTexture2);
BakedGlyph bakedGlyph2 = fontTexture2.add(glyphInfo);
this.textureManager.register(resourceLocation, fontTexture2);
return bakedGlyph2 == null ? this.missingGlyph : bakedGlyph2;
}
@Inject(method = "close", at = @At("TAIL"))
private void flywheel$close(CallbackInfo ci) {
flywheel$closeTextures();
}
@Unique
private void flywheel$closeTextures() {
for (AsyncFontTexture texture : flywheel$textures) {
texture.close();
}
flywheel$textures.clear();
}
}

View file

@ -12,3 +12,5 @@ out vec3 flw_vertexNormal;
out float flw_distance;
FlwMaterial flw_material;
#define flw_vertexId gl_VertexID

View file

@ -6,6 +6,8 @@
"refmap": "backend-flywheel.refmap.json",
"client": [
"AbstractClientPlayerAccessor",
"CodePointMapMixin",
"FontSetMixin",
"GlStateManagerMixin",
"LevelRendererAccessor",
"OptionsMixin",

View file

@ -0,0 +1,100 @@
package dev.engine_room.flywheel.lib.instance;
import org.joml.Matrix4f;
import dev.engine_room.flywheel.api.instance.InstanceHandle;
import dev.engine_room.flywheel.api.instance.InstanceType;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.util.FastColor;
public class GlyphInstance extends AbstractInstance {
public final Matrix4f pose = new Matrix4f();
public float u0;
public float u1;
public float v0;
public float v1;
public float x0;
public float x1;
public float x2;
public float x3;
public float y0;
public float y1;
public byte red = (byte) 0xFF;
public byte green = (byte) 0xFF;
public byte blue = (byte) 0xFF;
public byte alpha = (byte) 0xFF;
public int light = 0;
public GlyphInstance(InstanceType<?> type, InstanceHandle handle) {
super(type, handle);
}
public GlyphInstance setGlyph(BakedGlyph glyph, float x, float y, boolean italic) {
var glyphReader = FlwLibLink.INSTANCE.getGlyphExtension(glyph);
u0 = glyphReader.flywheel$u0();
u1 = glyphReader.flywheel$u1();
v0 = glyphReader.flywheel$v0();
v1 = glyphReader.flywheel$v1();
float left = glyphReader.flywheel$left();
float right = glyphReader.flywheel$right();
float up = glyphReader.flywheel$up();
float down = glyphReader.flywheel$down();
float f = x + left;
float g = x + right;
float h = up - 3.0f;
float j = down - 3.0f;
float k = y + h;
float l = y + j;
float m = italic ? 1.0f - 0.25f * h : 0.0f;
float n = italic ? 1.0f - 0.25f * j : 0.0f;
y0 = k;
y1 = l;
x0 = f + m;
x1 = f + n;
x2 = g + n;
x3 = g + m;
return this;
}
public GlyphInstance colorArgb(int argb) {
return color(FastColor.ARGB32.red(argb), FastColor.ARGB32.green(argb), FastColor.ARGB32.blue(argb), FastColor.ARGB32.alpha(argb));
}
public GlyphInstance colorRgb(int rgb) {
return color(FastColor.ARGB32.red(rgb), FastColor.ARGB32.green(rgb), FastColor.ARGB32.blue(rgb));
}
public GlyphInstance color(int red, int green, int blue, int alpha) {
return color((byte) red, (byte) green, (byte) blue, (byte) alpha);
}
public GlyphInstance color(int red, int green, int blue) {
return color((byte) red, (byte) green, (byte) blue);
}
public GlyphInstance color(byte red, byte green, byte blue, byte alpha) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
return this;
}
public GlyphInstance color(byte red, byte green, byte blue) {
this.red = red;
this.green = green;
this.blue = blue;
return this;
}
}

View file

@ -103,6 +103,37 @@ public final class InstanceTypes {
.cullShader(Flywheel.rl("instance/cull/shadow.glsl"))
.build();
public static final InstanceType<GlyphInstance> GLYPH = SimpleInstanceType.builder(GlyphInstance::new)
.layout(LayoutBuilder.create()
.matrix("pose", FloatRepr.FLOAT, 4)
.vector("u0u1v0v1", FloatRepr.FLOAT, 4)
.vector("x", FloatRepr.FLOAT, 4)
.vector("y", FloatRepr.FLOAT, 2)
.vector("color", FloatRepr.UNSIGNED_BYTE, 4)
.vector("light", FloatRepr.UNSIGNED_SHORT, 2)
.build())
.writer((ptr, instance) -> {
ExtraMemoryOps.putMatrix4f(ptr, instance.pose);
MemoryUtil.memPutFloat(ptr + 64, instance.u0);
MemoryUtil.memPutFloat(ptr + 68, instance.u1);
MemoryUtil.memPutFloat(ptr + 72, instance.v0);
MemoryUtil.memPutFloat(ptr + 76, instance.v1);
MemoryUtil.memPutFloat(ptr + 80, instance.x0);
MemoryUtil.memPutFloat(ptr + 84, instance.x1);
MemoryUtil.memPutFloat(ptr + 88, instance.x2);
MemoryUtil.memPutFloat(ptr + 92, instance.x3);
MemoryUtil.memPutFloat(ptr + 96, instance.y0);
MemoryUtil.memPutFloat(ptr + 100, instance.y1);
MemoryUtil.memPutByte(ptr + 104, instance.red);
MemoryUtil.memPutByte(ptr + 105, instance.green);
MemoryUtil.memPutByte(ptr + 106, instance.blue);
MemoryUtil.memPutByte(ptr + 107, instance.alpha);
ExtraMemoryOps.put2x16(ptr + 108, instance.light);
})
.vertexShader(Flywheel.rl("instance/glyph.vert"))
.cullShader(Flywheel.rl("instance/cull/glyph.glsl"))
.build();
private InstanceTypes() {
}
}

View file

@ -10,7 +10,11 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.engine_room.flywheel.api.internal.DependencyInjection;
import dev.engine_room.flywheel.lib.transform.PoseTransformStack;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.resources.ResourceLocation;
public interface FlwLibLink {
FlwLibLink INSTANCE = DependencyInjection.load(FlwLibLink.class, "dev.engine_room.flywheel.impl.FlwLibLinkImpl");
@ -24,4 +28,8 @@ public interface FlwLibLink {
void compileModelPart(ModelPart part, PoseStack.Pose pose, VertexConsumer consumer, int light, int overlay, float red, float green, float blue, float alpha);
Deque<PoseStack.Pose> getPoseStack(PoseStack stack);
GlyphExtension getGlyphExtension(BakedGlyph glyph);
FontSet getFontSet(Font font, ResourceLocation loc);
}

View file

@ -0,0 +1,25 @@
package dev.engine_room.flywheel.lib.internal;
import net.minecraft.resources.ResourceLocation;
public interface GlyphExtension {
float flywheel$u0();
float flywheel$u1();
float flywheel$v0();
float flywheel$v1();
float flywheel$left();
float flywheel$right();
float flywheel$up();
float flywheel$down();
ResourceLocation flywheel$texture();
void flywheel$texture(ResourceLocation location);
}

View file

@ -178,6 +178,7 @@ public class SimpleMaterial implements Material {
shaders = material.shaders();
fog = material.fog();
cutout = material.cutout();
light = material.light();
texture = material.texture();
blur = material.blur();
mipmap = material.mipmap();

View file

@ -0,0 +1,213 @@
package dev.engine_room.flywheel.lib.visual;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.joml.Vector4fc;
import com.google.common.collect.Lists;
import com.mojang.blaze3d.font.GlyphInfo;
import dev.engine_room.flywheel.api.instance.InstancerProvider;
import dev.engine_room.flywheel.api.material.Material;
import dev.engine_room.flywheel.api.model.Model;
import dev.engine_room.flywheel.api.vertex.MutableVertexList;
import dev.engine_room.flywheel.lib.instance.GlyphInstance;
import dev.engine_room.flywheel.lib.instance.InstanceTypes;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import dev.engine_room.flywheel.lib.material.CutoutShaders;
import dev.engine_room.flywheel.lib.material.SimpleMaterial;
import dev.engine_room.flywheel.lib.model.QuadMesh;
import dev.engine_room.flywheel.lib.model.SingleMeshModel;
import dev.engine_room.flywheel.lib.util.ResourceReloadCache;
import dev.engine_room.flywheel.lib.visual.util.SmartRecycler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.gui.font.glyphs.EmptyGlyph;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.network.chat.TextColor;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSink;
public class NameplateVisual {
public final Matrix4f pose = new Matrix4f();
private final InstancerProvider provider;
private final InstanceEmitter instanceEmitter;
@Nullable
private Component name;
public NameplateVisual(InstancerProvider provider) {
this.provider = provider;
instanceEmitter = new InstanceEmitter(provider);
instanceEmitter.font = Minecraft.getInstance().font;
}
public void name(Component name) {
this.name = name;
}
public void update() {
if (name == null) {
return;
}
instanceEmitter.recycler.resetCount();
instanceEmitter.x = 0;
instanceEmitter.y = 0;
name.getVisualOrderText()
.accept(instanceEmitter);
instanceEmitter.recycler.discardExtra();
}
private class InstanceEmitter implements FormattedCharSink {
private final SmartRecycler<GlyphModelKey, GlyphInstance> recycler;
Font font;
private boolean dropShadow;
private float dimFactor;
private byte r;
private byte g;
private byte b;
private byte a;
private Font.DisplayMode mode;
private int packedLightCoords;
float x;
float y;
private InstanceEmitter(InstancerProvider instancerProvider) {
recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key))
.createInstance());
}
@Nullable
private List<BakedGlyph.Effect> effects;
private void addEffect(BakedGlyph.Effect effect) {
if (this.effects == null) {
this.effects = Lists.newArrayList();
}
this.effects.add(effect);
}
@Override
public boolean accept(int i, Style style, int j) {
float n;
float l;
float h;
float g;
FontSet fontSet = FlwLibLink.INSTANCE.getFontSet(font, style.getFont());
GlyphInfo glyphInfo = fontSet.getGlyphInfo(j, false);
BakedGlyph bakedGlyph = style.isObfuscated() && j != 32 ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(j);
boolean bl = style.isBold();
float f = this.a;
TextColor textColor = style.getColor();
if (textColor != null) {
int k = textColor.getValue();
g = (float) (k >> 16 & 0xFF) / 255.0f * this.dimFactor;
h = (float) (k >> 8 & 0xFF) / 255.0f * this.dimFactor;
l = (float) (k & 0xFF) / 255.0f * this.dimFactor;
} else {
g = this.r;
h = this.g;
l = this.b;
}
if (!(bakedGlyph instanceof EmptyGlyph)) {
// var renderType = bakedGlyph.renderType(Font.DisplayMode.NORMAL);
var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph);
GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), bl, dropShadow));
glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic());
glyph.pose.set(pose);
glyph.light = LightTexture.FULL_BRIGHT;
glyph.setChanged();
}
float m = glyphInfo.getAdvance(bl);
float f2 = n = this.dropShadow ? 1.0f : 0.0f;
// if (style.isStrikethrough()) {
// this.addEffect(new BakedGlyph.Effect(this.x + n - 1.0f, this.y + n + 4.5f, this.x + n + m, this.y + n + 4.5f - 1.0f, 0.01f, g, h, l, f));
// }
// if (style.isUnderlined()) {
// this.addEffect(new BakedGlyph.Effect(this.x + n - 1.0f, this.y + n + 9.0f, this.x + n + m, this.y + n + 9.0f - 1.0f, 0.01f, g, h, l, f));
// }
this.x += m;
return true;
}
public float finish(int backgroundColor, float x) {
if (backgroundColor != 0) {
float f = (float) (backgroundColor >> 24 & 0xFF) / 255.0f;
float g = (float) (backgroundColor >> 16 & 0xFF) / 255.0f;
float h = (float) (backgroundColor >> 8 & 0xFF) / 255.0f;
float i = (float) (backgroundColor & 0xFF) / 255.0f;
this.addEffect(new BakedGlyph.Effect(x - 1.0f, this.y + 9.0f, this.x + 1.0f, this.y - 1.0f, 0.01f, g, h, i, f));
}
// if (this.effects != null) {
// BakedGlyph bakedGlyph = font.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
// VertexConsumer vertexConsumer = this.bufferSource.getBuffer(bakedGlyph.renderType(this.mode));
// for (BakedGlyph.Effect effect : this.effects) {
// bakedGlyph.renderEffect(effect, this.pose, vertexConsumer, this.packedLightCoords);
// }
// }
return this.x;
}
}
private static final ResourceReloadCache<GlyphModelKey, Model> GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into);
private static final Material GLYPH_MATERIAL = SimpleMaterial.builder()
.build();
private record GlyphModelKey(ResourceLocation font, boolean bold, boolean shadow) {
private Model into() {
// bold -> x + 1
// shadow -> x + 1, y + 1
return new SingleMeshModel(new GlyphMesh(new Vector3f[]{new Vector3f(0, 0, 0),}), SimpleMaterial.builderOf(GLYPH_MATERIAL)
.texture(font)
.cutout(CutoutShaders.ONE_TENTH)
.build());
}
}
public record GlyphMesh(Vector3f[] quads) implements QuadMesh {
@Override
public int vertexCount() {
return 4 * quads.length;
}
@Override
public void write(MutableVertexList vertexList) {
for (int i = 0; i < quads.length; i++) {
Vector3f quad = quads[i];
var quadStart = i * 4;
for (int j = 0; j < 4; j++) {
vertexList.x(quadStart + j, quad.x);
vertexList.y(quadStart + j, quad.y);
vertexList.z(quadStart + j, quad.z);
vertexList.normalX(quadStart + j, 0);
vertexList.normalY(quadStart + j, 0);
vertexList.normalZ(quadStart + j, 1);
}
}
}
@Override
public Vector4fc boundingSphere() {
return new Vector4f(0, 0, 0, 2);
}
}
}

View file

@ -0,0 +1,9 @@
#include "flywheel:util/matrix.glsl"
void flw_transformBoundingSphere(in FlwInstance i, inout vec3 center, inout float radius) {
radius += abs(i.x[2] - i.x[0]) + abs(i.y[1] - i.y[0]);
center += vec3((i.x[0] + i.x[2]) * 0.5, (i.y[0] + i.y[1]) * 0.5, 0.);
transformBoundingSphere(i.pose, center, radius);
}

View file

@ -0,0 +1,18 @@
void flw_instanceVertex(in FlwInstance i) {
uint vertexInGlyph = flw_vertexId % 4;
uint yIndex = ((vertexInGlyph + 1u) >> 1u) & 1u;
flw_vertexPos.x += i.x[vertexInGlyph];
flw_vertexPos.y += i.y[yIndex];
flw_vertexPos = i.pose * flw_vertexPos;
flw_vertexTexCoord.s = i.u0u1v0v1[(vertexInGlyph & 2u) >> 1u];
flw_vertexTexCoord.t = i.u0u1v0v1[2u + yIndex];
flw_vertexColor = i.color;
// Some drivers have a bug where uint over float division is invalid, so use an explicit cast.
flw_vertexLight = vec2(i.light) / 256.0;
}

View file

@ -9,11 +9,17 @@ import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.engine_room.flywheel.impl.extension.PoseStackExtension;
import dev.engine_room.flywheel.impl.mixin.FontAccessor;
import dev.engine_room.flywheel.impl.mixin.ModelPartAccessor;
import dev.engine_room.flywheel.impl.mixin.PoseStackAccessor;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import dev.engine_room.flywheel.lib.internal.GlyphExtension;
import dev.engine_room.flywheel.lib.transform.PoseTransformStack;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.resources.ResourceLocation;
public class FlwLibLinkImpl implements FlwLibLink {
@Override
@ -40,4 +46,14 @@ public class FlwLibLinkImpl implements FlwLibLink {
public Deque<PoseStack.Pose> getPoseStack(PoseStack stack) {
return ((PoseStackAccessor) stack).flywheel$getPoseStack();
}
@Override
public GlyphExtension getGlyphExtension(BakedGlyph glyph) {
return (GlyphExtension) glyph;
}
@Override
public FontSet getFontSet(Font font, ResourceLocation loc) {
return ((FontAccessor) font).flywheel$getFontSet(loc);
}
}

View file

@ -0,0 +1,91 @@
package dev.engine_room.flywheel.impl.mixin;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import dev.engine_room.flywheel.lib.internal.GlyphExtension;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.resources.ResourceLocation;
@Mixin(BakedGlyph.class)
public class BakedGlyphMixin implements GlyphExtension {
@Shadow
@Final
private float u0;
@Shadow
@Final
private float u1;
@Shadow
@Final
private float v0;
@Shadow
@Final
private float v1;
@Shadow
@Final
private float left;
@Shadow
@Final
private float right;
@Shadow
@Final
private float up;
@Shadow
@Final
private float down;
@Unique
private ResourceLocation flywheel$texture;
@Override
public float flywheel$u0() {
return u0;
}
@Override
public float flywheel$u1() {
return u1;
}
@Override
public float flywheel$v0() {
return v0;
}
@Override
public float flywheel$v1() {
return v1;
}
@Override
public float flywheel$left() {
return left;
}
@Override
public float flywheel$right() {
return right;
}
@Override
public float flywheel$up() {
return up;
}
@Override
public float flywheel$down() {
return down;
}
@Override
public ResourceLocation flywheel$texture() {
return flywheel$texture;
}
@Override
public void flywheel$texture(ResourceLocation location) {
flywheel$texture = location;
}
}

View file

@ -0,0 +1,14 @@
package dev.engine_room.flywheel.impl.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.resources.ResourceLocation;
@Mixin(Font.class)
public interface FontAccessor {
@Invoker("getFontSet")
FontSet flywheel$getFontSet(ResourceLocation fontLocation);
}

View file

@ -5,9 +5,11 @@
"compatibilityLevel": "JAVA_17",
"refmap": "flywheel.refmap.json",
"client": [
"BakedGlyphMixin",
"BlockEntityTypeMixin",
"ClientChunkCacheMixin",
"EntityTypeMixin",
"FontAccessor",
"LevelMixin",
"LevelRendererMixin",
"MinecraftMixin",