From 9227631c73f1afc1d918ad64accc1bdea77d84ee Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Tue, 24 Sep 2024 17:30:16 -0700 Subject: [PATCH 01/14] 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 --- .../flywheel/backend/engine/EngineImpl.java | 3 + .../backend/font/AsyncFontTexture.java | 159 +++++++++++++ .../backend/mixin/CodePointMapMixin.java | 146 ++++++++++++ .../flywheel/backend/mixin/FontSetMixin.java | 121 ++++++++++ .../flywheel/flywheel/internal/api_impl.vert | 2 + .../resources/flywheel.backend.mixins.json | 2 + .../flywheel/lib/instance/GlyphInstance.java | 100 ++++++++ .../flywheel/lib/instance/InstanceTypes.java | 31 +++ .../flywheel/lib/internal/FlwLibLink.java | 8 + .../flywheel/lib/internal/GlyphExtension.java | 25 ++ .../flywheel/lib/material/SimpleMaterial.java | 1 + .../flywheel/lib/visual/NameplateVisual.java | 213 ++++++++++++++++++ .../flywheel/instance/cull/glyph.glsl | 9 + .../flywheel/flywheel/instance/glyph.vert | 18 ++ .../flywheel/impl/FlwLibLinkImpl.java | 16 ++ .../flywheel/impl/mixin/BakedGlyphMixin.java | 91 ++++++++ .../flywheel/impl/mixin/FontAccessor.java | 14 ++ .../main/resources/flywheel.impl.mixins.json | 2 + 18 files changed, 961 insertions(+) create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java create mode 100644 common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl create mode 100644 common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert create mode 100644 common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java create mode 100644 common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java index 3a84e3a13..338a63991 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java @@ -2,6 +2,8 @@ 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 void onLightUpdate(SectionPos sectionPos, LightLayer layer) { @Override public void setupRender(RenderContext context) { try (var state = GlStateTracker.getRestoreState()) { + RenderSystem.replayQueue(); Uniforms.update(context); environmentStorage.flush(); drawManager.flush(lightStorage, environmentStorage); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java b/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java new file mode 100644 index 000000000..4189aa1d7 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java @@ -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 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); + } + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java new file mode 100644 index 000000000..27159cd61 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java @@ -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 { + @Shadow + @Final + private T[][] blockMap; + + @Shadow + @Final + private T[] empty; + + @Shadow + @Final + private IntFunction 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 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 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); + } + } + } + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java new file mode 100644 index 000000000..28a7cf5f7 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java @@ -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 glyphsByWidth; + + @Shadow + public abstract BakedGlyph getGlyph(int character); + + @Unique + private static final RandomSource RANDOM = RandomSource.createNewThreadLocalInstance(); + + @Unique + private List flywheel$textures; + + + @Inject(method = "", 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 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(); + } + +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert index d1e751929..019df0083 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert @@ -12,3 +12,5 @@ out vec3 flw_vertexNormal; out float flw_distance; FlwMaterial flw_material; + +#define flw_vertexId gl_VertexID diff --git a/common/src/backend/resources/flywheel.backend.mixins.json b/common/src/backend/resources/flywheel.backend.mixins.json index 2d924bef0..da4f3cfe7 100644 --- a/common/src/backend/resources/flywheel.backend.mixins.json +++ b/common/src/backend/resources/flywheel.backend.mixins.json @@ -6,6 +6,8 @@ "refmap": "backend-flywheel.refmap.json", "client": [ "AbstractClientPlayerAccessor", + "CodePointMapMixin", + "FontSetMixin", "GlStateManagerMixin", "LevelRendererAccessor", "OptionsMixin", diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java new file mode 100644 index 000000000..902ece1a1 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -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; + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index da093b634..0d271fcce 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -103,6 +103,37 @@ public final class InstanceTypes { .cullShader(Flywheel.rl("instance/cull/shadow.glsl")) .build(); + public static final InstanceType 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() { } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java index 9a112c07d..4899298c6 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java @@ -10,7 +10,11 @@ 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 getPoseStack(PoseStack stack); + + GlyphExtension getGlyphExtension(BakedGlyph glyph); + + FontSet getFontSet(Font font, ResourceLocation loc); } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java new file mode 100644 index 000000000..6d62472e1 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java @@ -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); +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java b/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java index cf271ed43..214712af4 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java @@ -178,6 +178,7 @@ public Builder copyFrom(Material material) { shaders = material.shaders(); fog = material.fog(); cutout = material.cutout(); + light = material.light(); texture = material.texture(); blur = material.blur(); mipmap = material.mipmap(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java new file mode 100644 index 000000000..3fb42a933 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java @@ -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 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 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 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); + } + } +} diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl new file mode 100644 index 000000000..8f49e6898 --- /dev/null +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl @@ -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); +} diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert new file mode 100644 index 000000000..89978637e --- /dev/null +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert @@ -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; +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java index 47fffeeb6..635ecc506 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java @@ -9,11 +9,17 @@ 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 void compileModelPart(ModelPart part, PoseStack.Pose pose, VertexConsumer public Deque 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); + } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java new file mode 100644 index 000000000..11dc3c27a --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java @@ -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; + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java new file mode 100644 index 000000000..dbea26326 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java @@ -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); +} diff --git a/common/src/main/resources/flywheel.impl.mixins.json b/common/src/main/resources/flywheel.impl.mixins.json index a1953a127..afae1ba3b 100644 --- a/common/src/main/resources/flywheel.impl.mixins.json +++ b/common/src/main/resources/flywheel.impl.mixins.json @@ -5,9 +5,11 @@ "compatibilityLevel": "JAVA_17", "refmap": "flywheel.refmap.json", "client": [ + "BakedGlyphMixin", "BlockEntityTypeMixin", "ClientChunkCacheMixin", "EntityTypeMixin", + "FontAccessor", "LevelMixin", "LevelRendererMixin", "MinecraftMixin", From 71cf582e971080c0c88ad3a61c34143940dba830 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Tue, 24 Sep 2024 21:00:45 -0700 Subject: [PATCH 02/14] Sign of life - Move all text handling into the TextVisual class - Add SignVisual - Flesh out the glyph model cache - Render effect glyphs - Clean up variable names --- .../flywheel/backend/engine/EngineImpl.java | 1 + .../flywheel/lib/instance/GlyphInstance.java | 4 + .../flywheel/lib/visual/NameplateVisual.java | 213 -------------- .../flywheel/lib/visual/TextVisual.java | 269 ++++++++++++++++++ .../flywheel/vanilla/SignVisual.java | 236 +++++++++++++++ .../flywheel/vanilla/VanillaVisuals.java | 3 + 6 files changed, 513 insertions(+), 213 deletions(-) delete mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java create mode 100644 common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java index 338a63991..47b58fae5 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java @@ -92,6 +92,7 @@ public void onLightUpdate(SectionPos sectionPos, LightLayer layer) { @Override public void setupRender(RenderContext context) { try (var state = GlStateTracker.getRestoreState()) { + // Process the render queue for font updates RenderSystem.replayQueue(); Uniforms.update(context); environmentStorage.flush(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java index 902ece1a1..cd33b677a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -83,6 +83,10 @@ public GlyphInstance color(int red, int green, int blue) { return color((byte) red, (byte) green, (byte) blue); } + public GlyphInstance color(float red, float green, float blue, float alpha) { + return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f), (byte) (alpha * 255f)); + } + public GlyphInstance color(byte red, byte green, byte blue, byte alpha) { this.red = red; this.green = green; diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java deleted file mode 100644 index 3fb42a933..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/NameplateVisual.java +++ /dev/null @@ -1,213 +0,0 @@ -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 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 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 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); - } - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java new file mode 100644 index 000000000..4ecc07c1b --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java @@ -0,0 +1,269 @@ +package dev.engine_room.flywheel.lib.visual; + +import java.util.ArrayList; +import java.util.List; + +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.joml.Vector4fc; + +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.texture.OverlayTexture; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.FormattedCharSink; + +/** + * A visual that renders a single line of text. + */ +public class TextVisual { + public boolean dropShadow; + public boolean with8xOutline; + public int backgroundColor = 0; + public int color; + public FormattedCharSequence content = FormattedCharSequence.EMPTY; + public float x; + public float y; + public int light; + + public final Matrix4f pose = new Matrix4f(); + + private final Sink sink; + + public TextVisual(InstancerProvider provider) { + sink = new Sink(provider); + } + + public void setup() { + sink.recycler.resetCount(); + sink.x = x; + sink.y = y; + sink.dimFactor = dropShadow ? 0.25f : 1.0f; + sink.r = (float) (color >> 16 & 0xFF) / 255.0f * sink.dimFactor; + sink.g = (float) (color >> 8 & 0xFF) / 255.0f * sink.dimFactor; + sink.b = (float) (color & 0xFF) / 255.0f * sink.dimFactor; + sink.a = (float) (color >> 24 & 0xFF) / 255.0f; + // FIXME: Need separate instances for the 8x outline and the center. + // Right now we just show the outline. + content.accept(sink); + sink.recycler.discardExtra(); + } + + public void delete() { + sink.recycler.delete(); + } + + private class Sink implements FormattedCharSink { + private final SmartRecycler recycler; + + Font font; + private float dimFactor; + private float r; + private float g; + private float b; + private float a; + + // Separate x and y from TextVisual because these advance as we accept glyphs + float x; + float y; + + private Sink(InstancerProvider instancerProvider) { + recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key)) + .createInstance()); + font = Minecraft.getInstance().font; + } + + @Override + public boolean accept(int i, Style style, int j) { + float b; + float g; + float r; + 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 bold = style.isBold(); + TextColor textColor = style.getColor(); + if (textColor != null) { + int color = textColor.getValue(); + r = (float) (color >> 16 & 0xFF) / 255.0f * this.dimFactor; + g = (float) (color >> 8 & 0xFF) / 255.0f * this.dimFactor; + b = (float) (color & 0xFF) / 255.0f * this.dimFactor; + } else { + r = this.r; + g = this.g; + b = this.b; + } + if (!(bakedGlyph instanceof EmptyGlyph)) { + var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); + + GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(bold, dropShadow, with8xOutline))); + + glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic()); + glyph.color(r, g, b, this.a); + glyph.pose.set(pose); + glyph.light = light; + glyph.setChanged(); + } + float advance = glyphInfo.getAdvance(bold); + float o = dropShadow ? 1.0f : 0.0f; + if (style.isStrikethrough()) { + this.addEffect(this.x + o - 1.0f, this.y + o + 4.5f, this.x + o + advance, this.y + o + 4.5f - 1.0f, 0.01f, r, g, b, this.a); + } + if (style.isUnderlined()) { + this.addEffect(this.x + o - 1.0f, this.y + o + 9.0f, this.x + o + advance, this.y + o + 9.0f - 1.0f, 0.01f, r, g, b, this.a); + } + this.x += advance; + return true; + } + + private void addEffect(float x0, float y0, float x1, float y1, float depth, float r, float g, float b, float a) { + BakedGlyph bakedGlyph = FlwLibLink.INSTANCE.getFontSet(font, Style.DEFAULT_FONT) + .whiteGlyph(); + + var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); + + GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(false, dropShadow, with8xOutline))); + + glyph.u0 = glyphExtension.flywheel$u0(); + glyph.u1 = glyphExtension.flywheel$u1(); + glyph.v0 = glyphExtension.flywheel$v0(); + glyph.v1 = glyphExtension.flywheel$v1(); + + glyph.y0 = y0; + glyph.y1 = y1; + + glyph.x0 = x0; + glyph.x1 = x1; + glyph.x2 = x1; + glyph.x3 = x0; + glyph.color(r, g, b, this.a); + glyph.pose.set(pose); + glyph.light = light; + glyph.setChanged(); + } + + 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(x - 1.0f, this.y + 9.0f, this.x + 1.0f, this.y - 1.0f, 0.01f, g, h, i, f); + } + return this.x; + } + } + + private static final ResourceReloadCache GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); + private static final ResourceReloadCache MESH_CACHE = new ResourceReloadCache<>(GlyphSettings::into); + + private static final Material GLYPH_MATERIAL = SimpleMaterial.builder() + .cutout(CutoutShaders.ONE_TENTH) + .backfaceCulling(false) + .build(); + + private record GlyphModelKey(ResourceLocation font, GlyphSettings settings) { + private Model into() { + return new SingleMeshModel(MESH_CACHE.get(settings), SimpleMaterial.builderOf(GLYPH_MATERIAL) + .texture(font) + .build()); + } + } + + // FIXME: probably replace with an enum + private record GlyphSettings(boolean bold, boolean dropShadow, boolean with8xOutline) { + public GlyphMesh into() { + // bold -> x + 1 + // shadow -> x + 1, y + 1 + + List out = new ArrayList<>(); + + if (with8xOutline) { + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + if (x == 0 && y == 0) { + continue; + } + + out.add(new Vector3f(x, y, 0)); + } + } + } else { + out.add(new Vector3f(0, 0, 0)); + } + + if (bold) { + out.add(new Vector3f(1, 0, 0)); + } + + if (dropShadow) { + out.add(new Vector3f(1, 1, 0)); + } + + return new GlyphMesh(out.toArray(new Vector3f[0])); + } + } + + /** + * A mesh that represents a single glyph. Expects to be drawn with the glyph instance type. + * + * @param quads Each quad will be expanded into 4 vertices. + */ + private 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); + vertexList.overlay(quadStart + j, OverlayTexture.NO_OVERLAY); + vertexList.r(quadStart + j, 1); + vertexList.g(quadStart + j, 1); + vertexList.b(quadStart + j, 1); + vertexList.a(quadStart + j, 1); + } + } + } + + @Override + public Vector4fc boundingSphere() { + // FIXME: what is the actual bounding sphere?? + return new Vector4f(0, 0, 0, 2); + } + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java new file mode 100644 index 000000000..1691591c3 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -0,0 +1,236 @@ +package dev.engine_room.flywheel.vanilla; + +import java.util.List; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; + +import dev.engine_room.flywheel.api.instance.Instance; +import dev.engine_room.flywheel.api.material.Material; +import dev.engine_room.flywheel.api.visualization.VisualizationContext; +import dev.engine_room.flywheel.lib.material.CutoutShaders; +import dev.engine_room.flywheel.lib.material.SimpleMaterial; +import dev.engine_room.flywheel.lib.model.part.InstanceTree; +import dev.engine_room.flywheel.lib.model.part.ModelTree; +import dev.engine_room.flywheel.lib.model.part.ModelTrees; +import dev.engine_room.flywheel.lib.util.ResourceReloadCache; +import dev.engine_room.flywheel.lib.visual.AbstractBlockEntityVisual; +import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual; +import dev.engine_room.flywheel.lib.visual.TextVisual; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.geom.ModelLayers; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.Sheets; +import net.minecraft.util.FastColor; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.SignBlock; +import net.minecraft.world.level.block.StandingSignBlock; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SignText; +import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.phys.Vec3; + +public class SignVisual extends AbstractBlockEntityVisual implements SimpleDynamicVisual { + + private static final Vec3 TEXT_OFFSET = new Vec3(0.0, 0.3333333432674408, 0.046666666865348816); + + private static final ResourceReloadCache SIGN_MODELS = new ResourceReloadCache<>(SignVisual::createSignModel); + + private static final Material MATERIAL = SimpleMaterial.builder() + .cutout(CutoutShaders.ONE_TENTH) + .texture(Sheets.SIGN_SHEET) + .backfaceCulling(false) + .build(); + + private final Matrix4f pose = new Matrix4f(); + private final InstanceTree instances; + + // The 8 lines of text we render + private final TextVisual[] frontText = new TextVisual[4]; + private final TextVisual[] backText = new TextVisual[4]; + + private SignText lastFrontText; + private SignText lastBackText; + + public SignVisual(VisualizationContext ctx, SignBlockEntity blockEntity, float partialTick) { + super(ctx, blockEntity, partialTick); + + for (int i = 0; i < 4; i++) { + frontText[i] = new TextVisual(ctx.instancerProvider()); + backText[i] = new TextVisual(ctx.instancerProvider()); + } + + var block = (SignBlock) blockState.getBlock(); + WoodType woodType = SignBlock.getWoodType(block); + var isStanding = block instanceof StandingSignBlock; + + instances = InstanceTree.create(ctx.instancerProvider(), SIGN_MODELS.get(woodType)); + + // Maybe use a separate model tree? + instances.childOrThrow("stick") + .visible(isStanding); + + var rotation = -block.getYRotationDegrees(blockState); + + var visualPosition = getVisualPosition(); + var signModelRenderScale = this.getSignModelRenderScale(); + pose.translate(visualPosition.getX() + 0.5f, visualPosition.getY() + 0.75f * signModelRenderScale, visualPosition.getZ() + 0.5f) + .rotateY(Mth.DEG_TO_RAD * rotation); + + if (!(isStanding)) { + pose.translate(0.0f, -0.3125f, -0.4375f); + } + + // Only apply this to the instances because text gets a separate scaling. + instances.scale(signModelRenderScale, -signModelRenderScale, -signModelRenderScale); + + instances.updateInstancesStatic(pose); + + lastFrontText = blockEntity.getFrontText(); + lastBackText = blockEntity.getBackText(); + this.setupText(lastFrontText, frontText, true); + this.setupText(lastBackText, backText, false); + } + + @Override + public void beginFrame(Context ctx) { + // Need to update every frame if the text is obfuscated + + if (lastFrontText != blockEntity.getFrontText()) { + lastFrontText = blockEntity.getFrontText(); + this.setupText(lastFrontText, frontText, true); + } + + if (lastBackText != blockEntity.getBackText()) { + lastBackText = blockEntity.getBackText(); + this.setupText(lastBackText, backText, false); + } + } + + @Override + public void collectCrumblingInstances(Consumer<@Nullable Instance> consumer) { + instances.traverse(consumer); + } + + @Override + public void updateLight(float partialTick) { + int packedLight = computePackedLight(); + instances.traverse(instance -> { + instance.light(packedLight) + .setChanged(); + }); + + // FIXME: fullbright + for (var text : frontText) { + text.light = packedLight; + } + for (var text : backText) { + text.light = packedLight; + } + } + + @Override + protected void _delete() { + instances.delete(); + + for (var text : frontText) { + text.delete(); + } + + for (var text : backText) { + text.delete(); + } + } + + public float getSignModelRenderScale() { + return 0.6666667f; + } + + public float getSignTextRenderScale() { + return 0.6666667f; + } + + public Vec3 getTextOffset() { + return TEXT_OFFSET; + } + + void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { + var font = Minecraft.getInstance().font; + FormattedCharSequence[] formattedCharSequences = text.getRenderMessages(Minecraft.getInstance() + .isTextFilteringEnabled(), component -> { + List list = font.split(component, blockEntity.getMaxTextLineWidth()); + return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); + }); + + int light; + boolean outline; + int darkColor = adjustColor(getDarkColor(text)); + int textColor; + if (text.hasGlowingText()) { + textColor = adjustColor(text.getColor() + .getTextColor()); + outline = true; + light = LightTexture.FULL_BRIGHT; + } else { + textColor = darkColor; + outline = false; + light = LightTexture.FULL_BLOCK; + } + + int lineHeight = blockEntity.getTextLineHeight(); + int lineDelta = 4 * lineHeight / 2; + for (int m = 0; m < 4; ++m) { + FormattedCharSequence formattedCharSequence = formattedCharSequences[m]; + float f = (float) -font.width(formattedCharSequence) / 2; + + var textVisual = dst[m]; + textVisual.color = textColor; + textVisual.dropShadow = false; + textVisual.with8xOutline = outline; + textVisual.backgroundColor = darkColor; + textVisual.x = f; + textVisual.y = m * lineHeight - lineDelta; + textVisual.content = formattedCharSequence; + // FIXME: separate flag for full bright? + textVisual.light = light; + + textVisual.pose.set(pose); + + if (!isFrontText) { + textVisual.pose.rotateY(Mth.PI); + } + var offset = getTextOffset(); + float scale = 0.015625f * this.getSignTextRenderScale(); + textVisual.pose.translate((float) offset.x, (float) offset.y, (float) offset.z); + textVisual.pose.scale(scale, -scale, scale); + + textVisual.setup(); + } + } + + private static int adjustColor(int color) { + if ((color & 0xFC000000) == 0) { + return color | 0xFF000000; + } + return color; + } + + static int getDarkColor(SignText signText) { + int i = signText.getColor() + .getTextColor(); + if (i == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) { + return -988212; + } + int j = (int) ((double) FastColor.ARGB32.red(i) * 0.4); + int k = (int) ((double) FastColor.ARGB32.green(i) * 0.4); + int l = (int) ((double) FastColor.ARGB32.blue(i) * 0.4); + return FastColor.ARGB32.color(0, j, k, l); + } + + private static ModelTree createSignModel(WoodType woodType) { + return ModelTrees.of(ModelLayers.createSignModelName(woodType), Sheets.getSignMaterial(woodType), MATERIAL); + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java index 53efc4562..cadda5114 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java @@ -47,6 +47,9 @@ public static void init() { .factory(ShulkerBoxVisual::new) .apply(); + builder(BlockEntityType.SIGN).factory(SignVisual::new) + .apply(); + builder(EntityType.CHEST_MINECART) .factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.CHEST_MINECART)) .skipVanillaRender(MinecartVisual::shouldSkipRender) From c1bf31856ff175dae505ca2d293e084206b4ff10 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Wed, 25 Sep 2024 19:36:32 -0700 Subject: [PATCH 03/14] Overwritten overwrites - Add mixinextras and remove overwrite - Merge AsyncFontTexture and FontTexture Signed-off-by: Jozufozu --- common/build.gradle.kts | 2 + .../backend/font/AsyncFontTexture.java | 159 ------------------ .../backend/mixin/CodePointMapMixin.java | 129 +++----------- .../flywheel/backend/mixin/FontSetMixin.java | 116 ++----------- .../mixin/FontTexture$NodeAccessor.java | 13 ++ .../backend/mixin/FontTextureMixin.java | 114 +++++++++++++ .../backend/util/FontTextureUpload.java | 10 ++ .../resources/flywheel.backend.mixins.json | 2 + .../lib/internal/FontTextureExtension.java | 7 + forge/build.gradle.kts | 3 + gradle.properties | 2 +- 11 files changed, 186 insertions(+), 371 deletions(-) delete mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java create mode 100644 common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 23d1f23b4..85e8f9878 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -72,6 +72,8 @@ jarSets { dependencies { modCompileOnly("net.fabricmc:fabric-loader:${property("fabric_loader_version")}") + compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!) + testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java b/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java deleted file mode 100644 index 4189aa1d7..000000000 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/font/AsyncFontTexture.java +++ /dev/null @@ -1,159 +0,0 @@ -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 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); - } - } -} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java index 27159cd61..0dfcd993c 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java @@ -1,146 +1,59 @@ 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 com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; + import net.minecraft.client.gui.font.CodepointMap; @Mixin(CodepointMap.class) public class CodePointMapMixin { - @Shadow - @Final - private T[][] blockMap; - - @Shadow - @Final - private T[] empty; - - @Shadow - @Final - private IntFunction blockConstructor; - @Unique private final Object flywheel$lock = new Object(); - /** - * @author - * @reason - */ - @Overwrite - public void clear() { + @WrapMethod(method = "clear") + private void flywheel$wrapClearAsSynchronized(Operation original) { synchronized (flywheel$lock) { - Arrays.fill(this.blockMap, this.empty); + original.call(); } } - /** - * @author - * @reason - */ - @Nullable - @Overwrite - public T get(int index) { - int i = index >> 8; - int j = index & 0xFF; + @WrapMethod(method = "get") + private T flywheel$wrapGetAsSynchronized(int index, Operation original) { synchronized (flywheel$lock) { - return this.blockMap[i][j]; + return original.call(index); } } - /** - * @author - * @reason - */ - @Nullable - @Overwrite - public T put(int index, T value) { - int i = index >> 8; - int j = index & 0xFF; - T object; + @WrapMethod(method = "put") + private T flywheel$wrapPutAsSynchronized(int index, T value, Operation original) { 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 original.call(index, value); } - return object; } - /** - * @author - * @reason - */ - @Overwrite - public T computeIfAbsent(int index, IntFunction valueIfAbsentGetter) { - int i = index >> 8; - int j = index & 0xFF; - T out; + @WrapMethod(method = "computeIfAbsent") + private T flywheel$wrapComputeIfAbsentAsSynchronized(int index, IntFunction valueIfAbsentGetter, Operation original) { 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 original.call(index, valueIfAbsentGetter); } - return out; } - /** - * @author - * @reason - */ - @Nullable - @Overwrite - public T remove(int index) { - int i = index >> 8; - int j = index & 0xFF; - T object; + @WrapMethod(method = "remove") + private T flywheel$wrapRemoveAsSynchronized(int index, Operation original) { synchronized (flywheel$lock) { - T[] objects = this.blockMap[i]; - if (objects == this.empty) { - return null; - } - object = objects[j]; - objects[j] = null; + return original.call(index); } - return object; } - /** - * @author - * @reason - */ - @Overwrite - public void forEach(CodepointMap.Output output) { + @WrapMethod(method = "forEach") + private void flywheel$wrapForEachAsSynchronized(CodepointMap.Output output, Operation original) { 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); - } - } + original.call(output); } } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java index 28a7cf5f7..39b53f211 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java @@ -1,121 +1,31 @@ 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 com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; -import dev.engine_room.flywheel.backend.font.AsyncFontTexture; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.IntList; +import dev.engine_room.flywheel.lib.internal.FontTextureExtension; 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.client.gui.font.FontTexture; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; @Mixin(FontSet.class) public abstract class FontSetMixin { + // Replace serial random with thread-local random @Shadow @Final - private TextureManager textureManager; - - @Shadow - private BakedGlyph missingGlyph; - - @Shadow - @Final - private ResourceLocation name; - - @Shadow - @Final - private Int2ObjectMap glyphsByWidth; - - @Shadow - public abstract BakedGlyph getGlyph(int character); - - @Unique - private static final RandomSource RANDOM = RandomSource.createNewThreadLocalInstance(); - - @Unique - private List flywheel$textures; - - - @Inject(method = "", at = @At("TAIL")) - public void init(TextureManager textureManager, ResourceLocation name, CallbackInfo ci) { - flywheel$textures = Lists.newArrayList(); + private static RandomSource RANDOM = RandomSource.createNewThreadLocalInstance(); + + @ModifyExpressionValue(method = "stitch", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/FontTexture")) + private FontTexture flywheel$setNameAfterCreate(FontTexture original, @Local ResourceLocation name) { + // Forward the name to the FontTexture so we can forward the name to the BakedGlyphs it creates. + // We need to know that to determine which Material to use when actually setting up instances. + ((FontTextureExtension) original).flywheel$setName(name); + return original; } - - @Inject(method = "reload", at = @At("TAIL")) - public void reload(List 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(); - } - } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java new file mode 100644 index 000000000..e8120e5ce --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java @@ -0,0 +1,13 @@ +package dev.engine_room.flywheel.backend.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(targets = "net.minecraft.client.gui.font.FontTexture$Node") +public interface FontTexture$NodeAccessor { + @Accessor("x") + int flywheel$getX(); + + @Accessor("y") + int flywheel$getY(); +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java new file mode 100644 index 000000000..37d63df74 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java @@ -0,0 +1,114 @@ +package dev.engine_room.flywheel.backend.mixin; + +import java.util.ArrayList; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import com.mojang.blaze3d.font.SheetGlyphInfo; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; + +import dev.engine_room.flywheel.backend.util.FontTextureUpload; +import dev.engine_room.flywheel.lib.internal.FontTextureExtension; +import dev.engine_room.flywheel.lib.internal.GlyphExtension; +import net.minecraft.client.gui.font.FontTexture; +import net.minecraft.client.gui.font.glyphs.BakedGlyph; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.resources.ResourceLocation; + +@Mixin(FontTexture.class) +public abstract class FontTextureMixin extends AbstractTexture implements FontTextureExtension { + @Unique + private final List flywheel$uploads = new ArrayList<>(); + @Unique + private boolean flywheel$flushScheduled = false; + + @Unique + private ResourceLocation flywheel$name; + + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;getId()I")) + private int flywheel$skipGetId(FontTexture instance, Operation original) { + // getId lazily creates the texture id, which is good, + // but it doesn't check for the render thread, which explodes. + if (RenderSystem.isOnRenderThreadOrInit()) { + return original.call(instance); + } + // We'll call getId manually in the recorded render call below. + return 0; + } + + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/TextureUtil;prepareImage(Lcom/mojang/blaze3d/platform/NativeImage$InternalGlFormat;III)V")) + private void flywheel$skipPrepareImage(NativeImage.InternalGlFormat arg, int i, int j, int k, Operation original) { + if (RenderSystem.isOnRenderThreadOrInit()) { + original.call(arg, i, j, k); + } else { + RenderSystem.recordRenderCall(() -> original.call(arg, getId(), j, k)); + } + } + + @WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;bind()V")) + private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload(FontTexture instance) { + return RenderSystem.isOnRenderThreadOrInit(); + } + + @WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V")) + private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload2(SheetGlyphInfo instance, int x, int y) { + return RenderSystem.isOnRenderThreadOrInit(); + } + + @WrapOperation(method = "add", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/font/FontTexture$Node;x:I", ordinal = 0)) + private int flywheel$shareNode(@Coerce Object instance, Operation original, @Share("node") LocalRef node) { + node.set(instance); + return original.call(instance); + } + + @Inject(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V", shift = At.Shift.AFTER)) + private void flywheel$uploadOrFlush(SheetGlyphInfo glyphInfo, CallbackInfoReturnable cir, @Share("node") LocalRef node) { + FontTexture$NodeAccessor accessor = ((FontTexture$NodeAccessor) node.get()); + + // Shove all the uploads into a list to be processed as a batch. + // Saves a lot of lambda allocations that would be spent binding the same texture over and over. + flywheel$uploads.add(new FontTextureUpload(glyphInfo, accessor.flywheel$getX(), accessor.flywheel$getY())); + + if (!flywheel$flushScheduled) { + RenderSystem.recordRenderCall(this::flywheel$flush); + flywheel$flushScheduled = true; + } + } + + @ModifyExpressionValue(method = "add", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/glyphs/BakedGlyph")) + private BakedGlyph flywheel$setGlyphExtensionName(BakedGlyph original) { + ((GlyphExtension) original).flywheel$texture(flywheel$name); + return original; + } + + @Unique + public void flywheel$flush() { + this.bind(); + for (FontTextureUpload upload : flywheel$uploads) { + upload.info() + .upload(upload.x(), upload.y()); + } + + flywheel$uploads.clear(); + + flywheel$flushScheduled = false; + } + + @Override + public void flywheel$setName(ResourceLocation value) { + flywheel$name = value; + } +} diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java b/common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java new file mode 100644 index 000000000..c6865dff9 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java @@ -0,0 +1,10 @@ +package dev.engine_room.flywheel.backend.util; + +import com.mojang.blaze3d.font.SheetGlyphInfo; + +/** + * For use in {@link dev.engine_room.flywheel.backend.mixin.FontTextureMixin} + * to batch glyph uploads when they're created in a flywheel worker thread. + */ +public record FontTextureUpload(SheetGlyphInfo info, int x, int y) { +} diff --git a/common/src/backend/resources/flywheel.backend.mixins.json b/common/src/backend/resources/flywheel.backend.mixins.json index da4f3cfe7..8a4f51c8f 100644 --- a/common/src/backend/resources/flywheel.backend.mixins.json +++ b/common/src/backend/resources/flywheel.backend.mixins.json @@ -8,6 +8,8 @@ "AbstractClientPlayerAccessor", "CodePointMapMixin", "FontSetMixin", + "FontTexture$NodeAccessor", + "FontTextureMixin", "GlStateManagerMixin", "LevelRendererAccessor", "OptionsMixin", diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java new file mode 100644 index 000000000..8b7b5cce7 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java @@ -0,0 +1,7 @@ +package dev.engine_room.flywheel.lib.internal; + +import net.minecraft.resources.ResourceLocation; + +public interface FontTextureExtension { + void flywheel$setName(ResourceLocation value); +} diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index c9569e26c..84d3ea2f0 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -87,6 +87,9 @@ dependencies { modCompileOnly("maven.modrinth:embeddium:${property("embeddium_version")}") modCompileOnly("maven.modrinth:oculus:${property("oculus_version")}") + compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!) + implementation(include("io.github.llamalad7:mixinextras-forge:0.4.1")!!) + "forApi"(project(path = ":common", configuration = "commonApiOnly")) "forLib"(project(path = ":common", configuration = "commonLib")) "forBackend"(project(path = ":common", configuration = "commonBackend")) diff --git a/gradle.properties b/gradle.properties index d8c4ff106..013c0181a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ parchment_version = 2023.09.03 # Minecraft build dependency versions minecraft_version = 1.20.1 forge_version = 47.2.19 -fabric_loader_version = 0.15.9 +fabric_loader_version=0.16.5 fabric_api_version = 0.92.1+1.20.1 # Build dependency mod versions From 8f9c57783b71f94c0343145ed265f9704db235f5 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Wed, 25 Sep 2024 22:53:28 -0700 Subject: [PATCH 04/14] A fine solution - Rely on transform matrix for all font vertex position stuffs - Use a skew matrix for italics --- .../flywheel/lib/instance/GlyphInstance.java | 48 +++++++++---------- .../flywheel/lib/instance/InstanceTypes.java | 18 ++----- .../flywheel/lib/visual/TextVisual.java | 32 +++++-------- .../flywheel/instance/cull/glyph.glsl | 4 -- .../flywheel/flywheel/instance/glyph.vert | 3 -- 5 files changed, 41 insertions(+), 64 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java index cd33b677a..510afd8bf 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -9,6 +9,10 @@ import net.minecraft.util.FastColor; public class GlyphInstance extends AbstractInstance { + // Skew x by 1 - 0.25 * y + // Note that columns are written as rows. + private static final Matrix4f ITALIC_SKEW = new Matrix4f(1, 0, 0, 0, -0.25f, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1); + public final Matrix4f pose = new Matrix4f(); public float u0; @@ -16,14 +20,6 @@ public class GlyphInstance extends AbstractInstance { 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; @@ -47,22 +43,26 @@ public GlyphInstance setGlyph(BakedGlyph glyph, float x, float y, boolean italic 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; + pose.translate(x + left, y + up - 3.0f, 0.0f); + pose.scale(right - left, down - up, 1.0f); + + if (italic) { + pose.mul(ITALIC_SKEW); + } + + return this; + } + + public GlyphInstance setEffect(BakedGlyph glyph, float x0, float y0, float x1, float y1, float depth) { + var glyphReader = FlwLibLink.INSTANCE.getGlyphExtension(glyph); + + u0 = glyphReader.flywheel$u0(); + u1 = glyphReader.flywheel$u1(); + v0 = glyphReader.flywheel$v0(); + v1 = glyphReader.flywheel$v1(); + + pose.translate(x0, y0, depth); + pose.scale(x1 - x0, y1 - y0, 1.0f); return this; } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index 0d271fcce..4002af4d2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -107,8 +107,6 @@ public final class InstanceTypes { .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()) @@ -118,17 +116,11 @@ public final class InstanceTypes { 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); + MemoryUtil.memPutByte(ptr + 80, instance.red); + MemoryUtil.memPutByte(ptr + 81, instance.green); + MemoryUtil.memPutByte(ptr + 82, instance.blue); + MemoryUtil.memPutByte(ptr + 83, instance.alpha); + ExtraMemoryOps.put2x16(ptr + 84, instance.light); }) .vertexShader(Flywheel.rl("instance/glyph.vert")) .cullShader(Flywheel.rl("instance/cull/glyph.glsl")) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java index 4ecc07c1b..7db6ca399 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java @@ -39,6 +39,8 @@ * A visual that renders a single line of text. */ public class TextVisual { + public static final float ONE_PIXEL = 0.125f; + public boolean dropShadow; public boolean with8xOutline; public int backgroundColor = 0; @@ -120,9 +122,9 @@ public boolean accept(int i, Style style, int j) { GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(bold, dropShadow, with8xOutline))); + glyph.pose.set(pose); glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic()); glyph.color(r, g, b, this.a); - glyph.pose.set(pose); glyph.light = light; glyph.setChanged(); } @@ -146,20 +148,9 @@ private void addEffect(float x0, float y0, float x1, float y1, float depth, floa GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(false, dropShadow, with8xOutline))); - glyph.u0 = glyphExtension.flywheel$u0(); - glyph.u1 = glyphExtension.flywheel$u1(); - glyph.v0 = glyphExtension.flywheel$v0(); - glyph.v1 = glyphExtension.flywheel$v1(); - - glyph.y0 = y0; - glyph.y1 = y1; - - glyph.x0 = x0; - glyph.x1 = x1; - glyph.x2 = x1; - glyph.x3 = x0; - glyph.color(r, g, b, this.a); glyph.pose.set(pose); + glyph.setEffect(bakedGlyph, x0, y0, x1, y1, depth); + glyph.color(r, g, b, this.a); glyph.light = light; glyph.setChanged(); } @@ -181,7 +172,6 @@ public float finish(int backgroundColor, float x) { private static final Material GLYPH_MATERIAL = SimpleMaterial.builder() .cutout(CutoutShaders.ONE_TENTH) - .backfaceCulling(false) .build(); private record GlyphModelKey(ResourceLocation font, GlyphSettings settings) { @@ -207,7 +197,7 @@ public GlyphMesh into() { continue; } - out.add(new Vector3f(x, y, 0)); + out.add(new Vector3f(x * ONE_PIXEL, y * ONE_PIXEL, 0)); } } } else { @@ -215,11 +205,11 @@ public GlyphMesh into() { } if (bold) { - out.add(new Vector3f(1, 0, 0)); + out.add(new Vector3f(ONE_PIXEL, 0, 0)); } if (dropShadow) { - out.add(new Vector3f(1, 1, 0)); + out.add(new Vector3f(ONE_PIXEL, ONE_PIXEL, 0)); } return new GlyphMesh(out.toArray(new Vector3f[0])); @@ -232,6 +222,8 @@ public GlyphMesh into() { * @param quads Each quad will be expanded into 4 vertices. */ private record GlyphMesh(Vector3f[] quads) implements QuadMesh { + private static final float[] X = new float[]{0, 0, 1, 1}; + private static final float[] Y = new float[]{0, 1, 1, 0}; @Override public int vertexCount() { @@ -245,8 +237,8 @@ public void write(MutableVertexList vertexList) { var quadStart = i * 4; for (int j = 0; j < 4; j++) { - vertexList.x(quadStart + j, quad.x); - vertexList.y(quadStart + j, quad.y); + vertexList.x(quadStart + j, quad.x + X[j]); + vertexList.y(quadStart + j, quad.y + Y[j]); vertexList.z(quadStart + j, quad.z); vertexList.normalX(quadStart + j, 0); vertexList.normalY(quadStart + j, 0); diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl index 8f49e6898..c8d9a275c 100644 --- a/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl @@ -1,9 +1,5 @@ #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); } diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert index 89978637e..3f7d2a303 100644 --- a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert @@ -3,9 +3,6 @@ void flw_instanceVertex(in FlwInstance i) { 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]; From 7aefbee9c12aae135da0a8ba87857cfbf159128f Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Wed, 25 Sep 2024 23:42:22 -0700 Subject: [PATCH 05/14] Glyphing in style - Fix glowing sign rendering - Use 2 instances per character - Move most glyph mesh state into the GlyphMode enum - GlyphSettings now has a GlyphMode and a bold boolean - Disable diffuse for text - Render normal text with polygon offset and bias the instancers for normal text so they appear in front of shadows or outlines - Make sink static - Fix glyph instance color being unnormalized --- .../flywheel/lib/instance/InstanceTypes.java | 2 +- .../flywheel/lib/visual/TextVisual.java | 108 ++++++++++++------ 2 files changed, 74 insertions(+), 36 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index 4002af4d2..4212e358f 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -107,7 +107,7 @@ public final class InstanceTypes { .layout(LayoutBuilder.create() .matrix("pose", FloatRepr.FLOAT, 4) .vector("u0u1v0v1", FloatRepr.FLOAT, 4) - .vector("color", FloatRepr.UNSIGNED_BYTE, 4) + .vector("color", FloatRepr.NORMALIZED_UNSIGNED_BYTE, 4) .vector("light", FloatRepr.UNSIGNED_SHORT, 2) .build()) .writer((ptr, instance) -> { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java index 7db6ca399..44a616d34 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java @@ -60,16 +60,21 @@ public TextVisual(InstancerProvider provider) { public void setup() { sink.recycler.resetCount(); + sink.pose = pose; + sink.light = light; + + if (with8xOutline) { + sink.x = x; + sink.y = y; + sink.setup(GlyphMode.OUTLINE, this.backgroundColor); + content.accept(sink); + } + + sink.setup(GlyphMode.SIMPLE, this.color); sink.x = x; sink.y = y; - sink.dimFactor = dropShadow ? 0.25f : 1.0f; - sink.r = (float) (color >> 16 & 0xFF) / 255.0f * sink.dimFactor; - sink.g = (float) (color >> 8 & 0xFF) / 255.0f * sink.dimFactor; - sink.b = (float) (color & 0xFF) / 255.0f * sink.dimFactor; - sink.a = (float) (color >> 24 & 0xFF) / 255.0f; - // FIXME: Need separate instances for the 8x outline and the center. - // Right now we just show the outline. content.accept(sink); + sink.recycler.discardExtra(); } @@ -77,26 +82,36 @@ public void delete() { sink.recycler.delete(); } - private class Sink implements FormattedCharSink { + private static class Sink implements FormattedCharSink { private final SmartRecycler recycler; - Font font; - private float dimFactor; + private final Font font; + private int light; + private Matrix4f pose; + private GlyphMode mode = GlyphMode.SIMPLE; private float r; private float g; private float b; private float a; // Separate x and y from TextVisual because these advance as we accept glyphs - float x; - float y; + private float x; + private float y; private Sink(InstancerProvider instancerProvider) { - recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key)) + recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key), key.settings.glyphMode.bias) .createInstance()); font = Minecraft.getInstance().font; } + private void setup(GlyphMode mode, int color) { + this.mode = mode; + r = (float) (color >> 16 & 0xFF) / 255.0f * mode.dimFactor; + g = (float) (color >> 8 & 0xFF) / 255.0f * mode.dimFactor; + b = (float) (color & 0xFF) / 255.0f * mode.dimFactor; + a = (float) (color >> 24 & 0xFF) / 255.0f; + } + @Override public boolean accept(int i, Style style, int j) { float b; @@ -109,9 +124,9 @@ public boolean accept(int i, Style style, int j) { TextColor textColor = style.getColor(); if (textColor != null) { int color = textColor.getValue(); - r = (float) (color >> 16 & 0xFF) / 255.0f * this.dimFactor; - g = (float) (color >> 8 & 0xFF) / 255.0f * this.dimFactor; - b = (float) (color & 0xFF) / 255.0f * this.dimFactor; + r = (float) (color >> 16 & 0xFF) / 255.0f * mode.dimFactor; + g = (float) (color >> 8 & 0xFF) / 255.0f * mode.dimFactor; + b = (float) (color & 0xFF) / 255.0f * mode.dimFactor; } else { r = this.r; g = this.g; @@ -120,7 +135,7 @@ public boolean accept(int i, Style style, int j) { if (!(bakedGlyph instanceof EmptyGlyph)) { var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); - GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(bold, dropShadow, with8xOutline))); + GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(mode, bold))); glyph.pose.set(pose); glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic()); @@ -129,7 +144,7 @@ public boolean accept(int i, Style style, int j) { glyph.setChanged(); } float advance = glyphInfo.getAdvance(bold); - float o = dropShadow ? 1.0f : 0.0f; + float o = mode.effectShift; if (style.isStrikethrough()) { this.addEffect(this.x + o - 1.0f, this.y + o + 4.5f, this.x + o + advance, this.y + o + 4.5f - 1.0f, 0.01f, r, g, b, this.a); } @@ -146,7 +161,7 @@ private void addEffect(float x0, float y0, float x1, float y1, float depth, floa var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); - GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(false, dropShadow, with8xOutline))); + GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(GlyphMode.SIMPLE, false))); glyph.pose.set(pose); glyph.setEffect(bakedGlyph, x0, y0, x1, y1, depth); @@ -172,47 +187,70 @@ public float finish(int backgroundColor, float x) { private static final Material GLYPH_MATERIAL = SimpleMaterial.builder() .cutout(CutoutShaders.ONE_TENTH) + .diffuse(false) .build(); private record GlyphModelKey(ResourceLocation font, GlyphSettings settings) { private Model into() { return new SingleMeshModel(MESH_CACHE.get(settings), SimpleMaterial.builderOf(GLYPH_MATERIAL) .texture(font) + .polygonOffset(settings.glyphMode.polygonOffset) .build()); } } - // FIXME: probably replace with an enum - private record GlyphSettings(boolean bold, boolean dropShadow, boolean with8xOutline) { - public GlyphMesh into() { - // bold -> x + 1 - // shadow -> x + 1, y + 1 + // This could probably be made a public interface and a TextVisual could render an arbitrary number of layers + private enum GlyphMode { + SIMPLE(1, 1, 0, true), + OUTLINE(1, 0, 0, false), + SHADOW(0.25f, 0, 1, false), + ; + + private final float dimFactor; + private final int bias; + private final float effectShift; + private final boolean polygonOffset; + + GlyphMode(float dimFactor, int bias, float effectShift, boolean polygonOffset) { + this.dimFactor = dimFactor; + this.bias = bias; + this.effectShift = effectShift; + this.polygonOffset = polygonOffset; + } + } + private record GlyphSettings(GlyphMode glyphMode, boolean bold) { + public GlyphMesh into() { List out = new ArrayList<>(); - if (with8xOutline) { + switch (glyphMode) { + case SIMPLE: + add(out, 0, 0); + break; + case OUTLINE: for (int x = -1; x <= 1; ++x) { for (int y = -1; y <= 1; ++y) { if (x == 0 && y == 0) { continue; } - out.add(new Vector3f(x * ONE_PIXEL, y * ONE_PIXEL, 0)); + add(out, x * ONE_PIXEL, y * ONE_PIXEL); } } - } else { - out.add(new Vector3f(0, 0, 0)); + break; + case SHADOW: + add(out, ONE_PIXEL, ONE_PIXEL); + break; } - if (bold) { - out.add(new Vector3f(ONE_PIXEL, 0, 0)); - } + return new GlyphMesh(out.toArray(new Vector3f[0])); + } - if (dropShadow) { - out.add(new Vector3f(ONE_PIXEL, ONE_PIXEL, 0)); + private void add(List out, float x, float y) { + out.add(new Vector3f(x, y, 0)); + if (bold) { + out.add(new Vector3f(x + ONE_PIXEL, y, 0)); } - - return new GlyphMesh(out.toArray(new Vector3f[0])); } } From c0b19a2f01f78de93171915dd36ca1cc068bf728 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Fri, 27 Sep 2024 14:20:02 -0700 Subject: [PATCH 06/14] Laying it on - TextVisual renders an arbitrary number of layers - Layers have relatively fine control over how the glyph instances are set up - Encapsulate all fields in TextVisual - Move Sinks to a thread local --- .../flywheel/lib/visual/TextVisual.java | 299 ----------------- .../lib/visual/text/SimpleTextLayer.java | 71 ++++ .../flywheel/lib/visual/text/TextLayer.java | 179 ++++++++++ .../flywheel/lib/visual/text/TextVisual.java | 310 ++++++++++++++++++ .../flywheel/vanilla/SignVisual.java | 61 ++-- 5 files changed, 593 insertions(+), 327 deletions(-) delete mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java deleted file mode 100644 index 44a616d34..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/TextVisual.java +++ /dev/null @@ -1,299 +0,0 @@ -package dev.engine_room.flywheel.lib.visual; - -import java.util.ArrayList; -import java.util.List; - -import org.joml.Matrix4f; -import org.joml.Vector3f; -import org.joml.Vector4f; -import org.joml.Vector4fc; - -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.texture.OverlayTexture; -import net.minecraft.network.chat.Style; -import net.minecraft.network.chat.TextColor; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.FormattedCharSequence; -import net.minecraft.util.FormattedCharSink; - -/** - * A visual that renders a single line of text. - */ -public class TextVisual { - public static final float ONE_PIXEL = 0.125f; - - public boolean dropShadow; - public boolean with8xOutline; - public int backgroundColor = 0; - public int color; - public FormattedCharSequence content = FormattedCharSequence.EMPTY; - public float x; - public float y; - public int light; - - public final Matrix4f pose = new Matrix4f(); - - private final Sink sink; - - public TextVisual(InstancerProvider provider) { - sink = new Sink(provider); - } - - public void setup() { - sink.recycler.resetCount(); - sink.pose = pose; - sink.light = light; - - if (with8xOutline) { - sink.x = x; - sink.y = y; - sink.setup(GlyphMode.OUTLINE, this.backgroundColor); - content.accept(sink); - } - - sink.setup(GlyphMode.SIMPLE, this.color); - sink.x = x; - sink.y = y; - content.accept(sink); - - sink.recycler.discardExtra(); - } - - public void delete() { - sink.recycler.delete(); - } - - private static class Sink implements FormattedCharSink { - private final SmartRecycler recycler; - - private final Font font; - private int light; - private Matrix4f pose; - private GlyphMode mode = GlyphMode.SIMPLE; - private float r; - private float g; - private float b; - private float a; - - // Separate x and y from TextVisual because these advance as we accept glyphs - private float x; - private float y; - - private Sink(InstancerProvider instancerProvider) { - recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key), key.settings.glyphMode.bias) - .createInstance()); - font = Minecraft.getInstance().font; - } - - private void setup(GlyphMode mode, int color) { - this.mode = mode; - r = (float) (color >> 16 & 0xFF) / 255.0f * mode.dimFactor; - g = (float) (color >> 8 & 0xFF) / 255.0f * mode.dimFactor; - b = (float) (color & 0xFF) / 255.0f * mode.dimFactor; - a = (float) (color >> 24 & 0xFF) / 255.0f; - } - - @Override - public boolean accept(int i, Style style, int j) { - float b; - float g; - float r; - 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 bold = style.isBold(); - TextColor textColor = style.getColor(); - if (textColor != null) { - int color = textColor.getValue(); - r = (float) (color >> 16 & 0xFF) / 255.0f * mode.dimFactor; - g = (float) (color >> 8 & 0xFF) / 255.0f * mode.dimFactor; - b = (float) (color & 0xFF) / 255.0f * mode.dimFactor; - } else { - r = this.r; - g = this.g; - b = this.b; - } - if (!(bakedGlyph instanceof EmptyGlyph)) { - var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); - - GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(mode, bold))); - - glyph.pose.set(pose); - glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic()); - glyph.color(r, g, b, this.a); - glyph.light = light; - glyph.setChanged(); - } - float advance = glyphInfo.getAdvance(bold); - float o = mode.effectShift; - if (style.isStrikethrough()) { - this.addEffect(this.x + o - 1.0f, this.y + o + 4.5f, this.x + o + advance, this.y + o + 4.5f - 1.0f, 0.01f, r, g, b, this.a); - } - if (style.isUnderlined()) { - this.addEffect(this.x + o - 1.0f, this.y + o + 9.0f, this.x + o + advance, this.y + o + 9.0f - 1.0f, 0.01f, r, g, b, this.a); - } - this.x += advance; - return true; - } - - private void addEffect(float x0, float y0, float x1, float y1, float depth, float r, float g, float b, float a) { - BakedGlyph bakedGlyph = FlwLibLink.INSTANCE.getFontSet(font, Style.DEFAULT_FONT) - .whiteGlyph(); - - var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); - - GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), new GlyphSettings(GlyphMode.SIMPLE, false))); - - glyph.pose.set(pose); - glyph.setEffect(bakedGlyph, x0, y0, x1, y1, depth); - glyph.color(r, g, b, this.a); - glyph.light = light; - glyph.setChanged(); - } - - 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(x - 1.0f, this.y + 9.0f, this.x + 1.0f, this.y - 1.0f, 0.01f, g, h, i, f); - } - return this.x; - } - } - - private static final ResourceReloadCache GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); - private static final ResourceReloadCache MESH_CACHE = new ResourceReloadCache<>(GlyphSettings::into); - - private static final Material GLYPH_MATERIAL = SimpleMaterial.builder() - .cutout(CutoutShaders.ONE_TENTH) - .diffuse(false) - .build(); - - private record GlyphModelKey(ResourceLocation font, GlyphSettings settings) { - private Model into() { - return new SingleMeshModel(MESH_CACHE.get(settings), SimpleMaterial.builderOf(GLYPH_MATERIAL) - .texture(font) - .polygonOffset(settings.glyphMode.polygonOffset) - .build()); - } - } - - // This could probably be made a public interface and a TextVisual could render an arbitrary number of layers - private enum GlyphMode { - SIMPLE(1, 1, 0, true), - OUTLINE(1, 0, 0, false), - SHADOW(0.25f, 0, 1, false), - ; - - private final float dimFactor; - private final int bias; - private final float effectShift; - private final boolean polygonOffset; - - GlyphMode(float dimFactor, int bias, float effectShift, boolean polygonOffset) { - this.dimFactor = dimFactor; - this.bias = bias; - this.effectShift = effectShift; - this.polygonOffset = polygonOffset; - } - } - - private record GlyphSettings(GlyphMode glyphMode, boolean bold) { - public GlyphMesh into() { - List out = new ArrayList<>(); - - switch (glyphMode) { - case SIMPLE: - add(out, 0, 0); - break; - case OUTLINE: - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - if (x == 0 && y == 0) { - continue; - } - - add(out, x * ONE_PIXEL, y * ONE_PIXEL); - } - } - break; - case SHADOW: - add(out, ONE_PIXEL, ONE_PIXEL); - break; - } - - return new GlyphMesh(out.toArray(new Vector3f[0])); - } - - private void add(List out, float x, float y) { - out.add(new Vector3f(x, y, 0)); - if (bold) { - out.add(new Vector3f(x + ONE_PIXEL, y, 0)); - } - } - } - - /** - * A mesh that represents a single glyph. Expects to be drawn with the glyph instance type. - * - * @param quads Each quad will be expanded into 4 vertices. - */ - private record GlyphMesh(Vector3f[] quads) implements QuadMesh { - private static final float[] X = new float[]{0, 0, 1, 1}; - private static final float[] Y = new float[]{0, 1, 1, 0}; - - @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 + X[j]); - vertexList.y(quadStart + j, quad.y + Y[j]); - vertexList.z(quadStart + j, quad.z); - vertexList.normalX(quadStart + j, 0); - vertexList.normalY(quadStart + j, 0); - vertexList.normalZ(quadStart + j, 1); - vertexList.overlay(quadStart + j, OverlayTexture.NO_OVERLAY); - vertexList.r(quadStart + j, 1); - vertexList.g(quadStart + j, 1); - vertexList.b(quadStart + j, 1); - vertexList.a(quadStart + j, 1); - } - } - } - - @Override - public Vector4fc boundingSphere() { - // FIXME: what is the actual bounding sphere?? - return new Vector4f(0, 0, 0, 2); - } - } -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java new file mode 100644 index 000000000..8b2943929 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java @@ -0,0 +1,71 @@ +package dev.engine_room.flywheel.lib.visual.text; + +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public record SimpleTextLayer(GlyphMeshStyle style, GlyphMaterial material, GlyphColor color, int bias, float offsetX, + float offsetY, float effectOffsetX, float effectOffsetY) implements TextLayer { + public static class Builder { + @Nullable + private GlyphMeshStyle style; + @Nullable + private GlyphMaterial material; + @Nullable + private GlyphColor color; + + private int bias; + private float offsetX = 0; + private float offsetY = 0; + private float effectOffsetX = 1; + private float effectOffsetY = 1; + + public Builder style(GlyphMeshStyle style) { + this.style = style; + return this; + } + + public Builder material(GlyphMaterial material) { + this.material = material; + return this; + } + + public Builder color(GlyphColor color) { + this.color = color; + return this; + } + + public Builder bias(int bias) { + this.bias = bias; + return this; + } + + public Builder offsetX(float offsetX) { + this.offsetX = offsetX; + return this; + } + + public Builder offsetY(float offsetY) { + this.offsetY = offsetY; + return this; + } + + public Builder effectOffsetX(float effectOffsetX) { + this.effectOffsetX = effectOffsetX; + return this; + } + + public Builder effectOffsetY(float effectOffsetY) { + this.effectOffsetY = effectOffsetY; + return this; + } + + public SimpleTextLayer build() { + Objects.requireNonNull(style); + Objects.requireNonNull(material); + Objects.requireNonNull(color); + + return new SimpleTextLayer(style, material, color, bias, offsetX, offsetY, effectOffsetX, effectOffsetY); + } + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java new file mode 100644 index 000000000..22c27854e --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java @@ -0,0 +1,179 @@ +package dev.engine_room.flywheel.lib.visual.text; + +import java.util.function.Consumer; + +import org.joml.Vector3f; + +import dev.engine_room.flywheel.api.material.Material; +import dev.engine_room.flywheel.lib.material.CutoutShaders; +import dev.engine_room.flywheel.lib.material.SimpleMaterial; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.resources.ResourceLocation; + +public interface TextLayer { + float ONE_PIXEL = 0.125f; + + /** + * The style of individual glyphs. + * + * @return A GlyphMeshStyle. + */ + GlyphMeshStyle style(); + + /** + * A mapping from texture ResourceLocations to Flywheel materials. + * + * @return A GlyphMaterial. + */ + GlyphMaterial material(); + + /** + * A mapping from text styles to ARGB colors. + * + * @return A GlyphColor. + */ + GlyphColor color(); + + /** + * The instancer bias for this layer. + * + * @return The bias. + */ + int bias(); + + /** + * The x offset of text content in this layer. + * + * @return The x offset. + */ + float offsetX(); + + /** + * The y offset of text content in this layer. + * + * @return The y offset. + */ + float offsetY(); + + /** + * The x offset of text effects such as strikethrough or underline in this layer. + * + * @return The x offset. + */ + float effectOffsetX(); + + /** + * The y offset of text effects such as strikethrough or underline in this layer. + * + * @return The y offset. + */ + float effectOffsetY(); + + @FunctionalInterface + interface GlyphColor { + /** + * Default to the given color if no color is specified in the style. + * + * @param color The ARGB color to default to. + * @return A new GlyphColor. + */ + static GlyphColor defaultTo(int color) { + return style -> { + TextColor textColor = style.getColor(); + if (textColor != null) { + return adjustColor(textColor.getValue()); + } + return color; + }; + } + + /** + * Always use the given color, regardless of the style. + * + * @param color The ARGB color to use. + * @return A new GlyphColor. + */ + static GlyphColor always(int color) { + return style -> adjustColor(color); + } + + /** + * Adjust the color to be fully opaque if it's very close to having 0 alpha. + * + * @param color The ARGB color to adjust. + * @return The adjusted color. + */ + static int adjustColor(int color) { + if ((color & 0xFC000000) == 0) { + return color | 0xFF000000; + } + return color; + } + + /** + * Convert a style to a color. + * + * @param style The style of the text to colorize. + * @return The color to use, in ARGB format. + */ + int color(Style style); + } + + @FunctionalInterface + interface GlyphMaterial { + GlyphMaterial SIMPLE = texture -> SimpleMaterial.builder() + .texture(texture) + .cutout(CutoutShaders.ONE_TENTH) + .diffuse(false) + .build(); + + GlyphMaterial POLYGON_OFFSET = texture -> SimpleMaterial.builder() + .texture(texture) + .cutout(CutoutShaders.ONE_TENTH) + .diffuse(false) + .polygonOffset(true) + .build(); + + /** + * Create a Flywheel material for the given glyph texture. + * + * @param texture The texture to use. + * @return A material. + */ + Material create(ResourceLocation texture); + } + + @FunctionalInterface + interface GlyphMeshStyle { + /** + * The standard style for glyphs with no repetition. + */ + GlyphMeshStyle SIMPLE = out -> out.accept(new Vector3f(0, 0, 0)); + + /** + * The style for glyphs with a 8x outline as used by glowing text on signs. + */ + GlyphMeshStyle OUTLINE = out -> { + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + if (x == 0 && y == 0) { + continue; + } + + out.accept(new Vector3f(x * ONE_PIXEL, y * ONE_PIXEL, 0)); + } + } + }; + + /** + * Add quads to the mesh. Each vec3 submitted to out will be expanded + * into a unit quad in the XY plane with the lowest corner at the given vec3. + * You can think of each submitted vec3 as a duplication of a glyph. + * + * @param out The consumer to accept the quads + */ + void addQuads(Consumer out); + + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java new file mode 100644 index 000000000..32c6943bc --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -0,0 +1,310 @@ +package dev.engine_room.flywheel.lib.visual.text; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jetbrains.annotations.UnknownNullability; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.joml.Vector4fc; + +import com.mojang.blaze3d.font.GlyphInfo; + +import dev.engine_room.flywheel.api.instance.InstancerProvider; +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.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.client.renderer.texture.OverlayTexture; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.FormattedCharSink; + +/** + * A visual that renders a single line of text. + */ +public class TextVisual { + private static final ThreadLocal SINKS = ThreadLocal.withInitial(Sink::new); + + private final SmartRecycler recycler; + + private FormattedCharSequence content = FormattedCharSequence.EMPTY; + private float x; + private float y; + private int backgroundColor = 0; + private int light; + private boolean fullBright; + + private final List layers = new ArrayList<>(); + + private final Matrix4f pose = new Matrix4f(); + + public TextVisual(InstancerProvider provider) { + recycler = new SmartRecycler<>(key -> provider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key.modelKey), key.bias) + .createInstance()); + } + + public TextVisual content(FormattedCharSequence content) { + this.content = content; + return this; + } + + public Matrix4f pose() { + return pose; + } + + public TextVisual clearLayers() { + layers.clear(); + return this; + } + + public TextVisual addLayer(TextLayer layer) { + layers.add(layer); + return this; + } + + public TextVisual layers(Collection layers) { + this.layers.clear(); + this.layers.addAll(layers); + return this; + } + + public TextVisual pos(float x, float y) { + this.x = x; + this.y = y; + return this; + } + + public TextVisual x(float x) { + this.x = x; + return this; + } + + public TextVisual y(float y) { + this.y = y; + return this; + } + + public TextVisual backgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + return this; + } + + public TextVisual light(int light) { + this.light = light; + return this; + } + + public TextVisual fullBright(boolean fullBright) { + this.fullBright = fullBright; + return this; + } + + // TODO: method to just update pose or light without recalculating text + public void setup() { + recycler.resetCount(); + + var sink = SINKS.get(); + + var light = fullBright ? LightTexture.FULL_BRIGHT : this.light; + sink.prepare(recycler, pose, light); + + int maxX = 0; + // Can we flip the inner and outer loops here? + // Would that even be better? + for (TextLayer layer : layers) { + sink.x = x; + sink.y = y; + sink.layer = layer; + content.accept(sink); + maxX = Math.max(maxX, (int) sink.x); + } + + sink.addBackground(backgroundColor, x, maxX); + + sink.clear(); + + recycler.discardExtra(); + } + + public void delete() { + recycler.delete(); + } + + private static class Sink implements FormattedCharSink { + private final Font font; + + @UnknownNullability + private SmartRecycler recycler; + @UnknownNullability + private Matrix4f pose; + @UnknownNullability + private TextLayer layer; + + private int light; + + private float x; + private float y; + + private Sink() { + font = Minecraft.getInstance().font; + } + + private void prepare(SmartRecycler recycler, Matrix4f pose, int light) { + this.recycler = recycler; + this.pose = pose; + this.light = light; + } + + private void clear() { + recycler = null; + pose = null; + layer = null; + } + + @Override + public boolean accept(int i, Style style, int j) { + 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 bold = style.isBold(); + + int color = layer.color() + .color(style); + + if (!(bakedGlyph instanceof EmptyGlyph)) { + var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); + + GlyphInstance glyph = recycler.get(key(glyphExtension.flywheel$texture(), bold, layer.style())); + + glyph.pose.set(pose); + glyph.setGlyph(bakedGlyph, this.x + layer.offsetX(), this.y + layer.offsetY(), style.isItalic()); + glyph.colorArgb(color); + glyph.light = light; + glyph.setChanged(); + } + float advance = glyphInfo.getAdvance(bold); + float effectX = layer.effectOffsetX(); + float effectY = layer.effectOffsetY(); + if (style.isStrikethrough()) { + this.addEffect(this.x + effectX - 1.0f, this.y + effectY + 4.5f, this.x + effectX + advance, this.y + effectY + 4.5f - 1.0f, 0.01f, color); + } + if (style.isUnderlined()) { + this.addEffect(this.x + effectX - 1.0f, this.y + effectY + 9.0f, this.x + effectX + advance, this.y + effectY + 9.0f - 1.0f, 0.01f, color); + } + this.x += advance; + return true; + } + + private void addEffect(float x0, float y0, float x1, float y1, float depth, int colorArgb) { + BakedGlyph bakedGlyph = FlwLibLink.INSTANCE.getFontSet(font, Style.DEFAULT_FONT) + .whiteGlyph(); + + var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); + + GlyphInstance glyph = recycler.get(key(glyphExtension.flywheel$texture(), false, TextLayer.GlyphMeshStyle.SIMPLE)); + + glyph.pose.set(pose); + glyph.setEffect(bakedGlyph, x0, y0, x1, y1, depth); + glyph.colorArgb(colorArgb); + glyph.light = light; + glyph.setChanged(); + } + + public void addBackground(int backgroundColor, float startX, float endX) { + if (backgroundColor != 0) { + this.addEffect(startX - 1.0f, this.y + 9.0f, endX + 1.0f, this.y - 1.0f, 0.01f, backgroundColor); + } + } + + private GlyphInstancerKey key(ResourceLocation texture, boolean bold, TextLayer.GlyphMeshStyle style) { + var meshKey = new GlyphMeshKey(style, bold); + var modelKey = new GlyphModelKey(texture, meshKey, layer.material()); + return new GlyphInstancerKey(modelKey, layer.bias()); + } + } + + private record GlyphInstancerKey(GlyphModelKey modelKey, int bias) { + } + + private static final ResourceReloadCache GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); + private static final ResourceReloadCache MESH_CACHE = new ResourceReloadCache<>(GlyphMeshKey::into); + + private record GlyphModelKey(ResourceLocation font, GlyphMeshKey meshKey, TextLayer.GlyphMaterial material) { + private Model into() { + return new SingleMeshModel(MESH_CACHE.get(meshKey), material.create(font)); + } + } + + private record GlyphMeshKey(TextLayer.GlyphMeshStyle style, boolean bold) { + public GlyphMesh into() { + List out = new ArrayList<>(); + + style.addQuads(quad -> { + out.add(quad); + if (bold) { + out.add(new Vector3f(quad.x + TextLayer.ONE_PIXEL, quad.y, quad.z)); + } + }); + + return new GlyphMesh(out.toArray(new Vector3f[0])); + } + } + + /** + * A mesh that represents a single glyph. Expects to be drawn with the glyph instance type. + * + * @param quads Each quad will be expanded into 4 vertices. + */ + private record GlyphMesh(Vector3f[] quads) implements QuadMesh { + private static final float[] X = new float[]{0, 0, 1, 1}; + private static final float[] Y = new float[]{0, 1, 1, 0}; + + @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 + X[j]); + vertexList.y(quadStart + j, quad.y + Y[j]); + vertexList.z(quadStart + j, quad.z); + vertexList.normalX(quadStart + j, 0); + vertexList.normalY(quadStart + j, 0); + vertexList.normalZ(quadStart + j, 1); + vertexList.overlay(quadStart + j, OverlayTexture.NO_OVERLAY); + vertexList.r(quadStart + j, 1); + vertexList.g(quadStart + j, 1); + vertexList.b(quadStart + j, 1); + vertexList.a(quadStart + j, 1); + } + } + } + + @Override + public Vector4fc boundingSphere() { + // FIXME: what is the actual bounding sphere?? + return new Vector4f(0, 0, 0, 2); + } + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java index 1691591c3..ad03a5f13 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -1,5 +1,6 @@ package dev.engine_room.flywheel.vanilla; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -17,10 +18,11 @@ import dev.engine_room.flywheel.lib.util.ResourceReloadCache; import dev.engine_room.flywheel.lib.visual.AbstractBlockEntityVisual; import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual; -import dev.engine_room.flywheel.lib.visual.TextVisual; +import dev.engine_room.flywheel.lib.visual.text.SimpleTextLayer; +import dev.engine_room.flywheel.lib.visual.text.TextLayer; +import dev.engine_room.flywheel.lib.visual.text.TextVisual; import net.minecraft.client.Minecraft; import net.minecraft.client.model.geom.ModelLayers; -import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.Sheets; import net.minecraft.util.FastColor; import net.minecraft.util.FormattedCharSequence; @@ -123,12 +125,11 @@ public void updateLight(float partialTick) { .setChanged(); }); - // FIXME: fullbright for (var text : frontText) { - text.light = packedLight; + text.light(packedLight); } for (var text : backText) { - text.light = packedLight; + text.light(packedLight); } } @@ -158,54 +159,58 @@ public Vec3 getTextOffset() { } void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { - var font = Minecraft.getInstance().font; FormattedCharSequence[] formattedCharSequences = text.getRenderMessages(Minecraft.getInstance() .isTextFilteringEnabled(), component -> { - List list = font.split(component, blockEntity.getMaxTextLineWidth()); + List list = Minecraft.getInstance().font.split(component, blockEntity.getMaxTextLineWidth()); return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); }); - int light; - boolean outline; + List layers = new ArrayList<>(); + int darkColor = adjustColor(getDarkColor(text)); int textColor; if (text.hasGlowingText()) { textColor = adjustColor(text.getColor() .getTextColor()); - outline = true; - light = LightTexture.FULL_BRIGHT; + + layers.add(new SimpleTextLayer.Builder().style(TextLayer.GlyphMeshStyle.OUTLINE) + .material(TextLayer.GlyphMaterial.SIMPLE) + .color(TextLayer.GlyphColor.always(darkColor)) + .build()); } else { textColor = darkColor; - outline = false; - light = LightTexture.FULL_BLOCK; } + layers.add(new SimpleTextLayer.Builder().style(TextLayer.GlyphMeshStyle.SIMPLE) + .material(TextLayer.GlyphMaterial.POLYGON_OFFSET) + .color(TextLayer.GlyphColor.defaultTo(textColor)) + .bias(1) + .build()); + int lineHeight = blockEntity.getTextLineHeight(); int lineDelta = 4 * lineHeight / 2; for (int m = 0; m < 4; ++m) { FormattedCharSequence formattedCharSequence = formattedCharSequences[m]; - float f = (float) -font.width(formattedCharSequence) / 2; + float f = (float) -Minecraft.getInstance().font.width(formattedCharSequence) / 2; + + var textVisual = dst[m].content(formattedCharSequence) + .layers(layers) + .fullBright(text.hasGlowingText()) + .backgroundColor(0) + .x(f) + .y(m * lineHeight - lineDelta); - var textVisual = dst[m]; - textVisual.color = textColor; - textVisual.dropShadow = false; - textVisual.with8xOutline = outline; - textVisual.backgroundColor = darkColor; - textVisual.x = f; - textVisual.y = m * lineHeight - lineDelta; - textVisual.content = formattedCharSequence; - // FIXME: separate flag for full bright? - textVisual.light = light; + var textPose = textVisual.pose(); - textVisual.pose.set(pose); + textPose.set(pose); if (!isFrontText) { - textVisual.pose.rotateY(Mth.PI); + textPose.rotateY(Mth.PI); } var offset = getTextOffset(); float scale = 0.015625f * this.getSignTextRenderScale(); - textVisual.pose.translate((float) offset.x, (float) offset.y, (float) offset.z); - textVisual.pose.scale(scale, -scale, scale); + textPose.translate((float) offset.x, (float) offset.y, (float) offset.z); + textPose.scale(scale, -scale, scale); textVisual.setup(); } From 9515462d0dbf1f11d20c3a1e46c3313be469cdf2 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Fri, 27 Sep 2024 14:31:41 -0700 Subject: [PATCH 07/14] Get impl'd - Move all font/text related mixins into impl --- common/src/backend/resources/flywheel.backend.mixins.json | 4 ---- .../dev/engine_room/flywheel/impl/FlwLibLinkImpl.java | 2 +- .../dev/engine_room/flywheel/impl}/FontTextureUpload.java | 4 ++-- .../flywheel/impl/mixin/{ => text}/BakedGlyphMixin.java | 2 +- .../flywheel/impl/mixin/text}/CodePointMapMixin.java | 2 +- .../flywheel/impl/mixin/{ => text}/FontAccessor.java | 2 +- .../flywheel/impl/mixin/text}/FontSetMixin.java | 2 +- .../impl/mixin/text}/FontTexture$NodeAccessor.java | 2 +- .../flywheel/impl/mixin/text}/FontTextureMixin.java | 4 ++-- common/src/main/resources/flywheel.impl.mixins.json | 8 ++++++-- 10 files changed, 16 insertions(+), 16 deletions(-) rename common/src/{backend/java/dev/engine_room/flywheel/backend/util => main/java/dev/engine_room/flywheel/impl}/FontTextureUpload.java (62%) rename common/src/main/java/dev/engine_room/flywheel/impl/mixin/{ => text}/BakedGlyphMixin.java (96%) rename common/src/{backend/java/dev/engine_room/flywheel/backend/mixin => main/java/dev/engine_room/flywheel/impl/mixin/text}/CodePointMapMixin.java (97%) rename common/src/main/java/dev/engine_room/flywheel/impl/mixin/{ => text}/FontAccessor.java (87%) rename common/src/{backend/java/dev/engine_room/flywheel/backend/mixin => main/java/dev/engine_room/flywheel/impl/mixin/text}/FontSetMixin.java (96%) rename common/src/{backend/java/dev/engine_room/flywheel/backend/mixin => main/java/dev/engine_room/flywheel/impl/mixin/text}/FontTexture$NodeAccessor.java (84%) rename common/src/{backend/java/dev/engine_room/flywheel/backend/mixin => main/java/dev/engine_room/flywheel/impl/mixin/text}/FontTextureMixin.java (97%) diff --git a/common/src/backend/resources/flywheel.backend.mixins.json b/common/src/backend/resources/flywheel.backend.mixins.json index 8a4f51c8f..2d924bef0 100644 --- a/common/src/backend/resources/flywheel.backend.mixins.json +++ b/common/src/backend/resources/flywheel.backend.mixins.json @@ -6,10 +6,6 @@ "refmap": "backend-flywheel.refmap.json", "client": [ "AbstractClientPlayerAccessor", - "CodePointMapMixin", - "FontSetMixin", - "FontTexture$NodeAccessor", - "FontTextureMixin", "GlStateManagerMixin", "LevelRendererAccessor", "OptionsMixin", diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java index 635ecc506..f3abee51b 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java @@ -9,9 +9,9 @@ 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.impl.mixin.text.FontAccessor; import dev.engine_room.flywheel.lib.internal.FlwLibLink; import dev.engine_room.flywheel.lib.internal.GlyphExtension; import dev.engine_room.flywheel.lib.transform.PoseTransformStack; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java b/common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java similarity index 62% rename from common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java rename to common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java index c6865dff9..eb1c15ee2 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/util/FontTextureUpload.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java @@ -1,9 +1,9 @@ -package dev.engine_room.flywheel.backend.util; +package dev.engine_room.flywheel.impl; import com.mojang.blaze3d.font.SheetGlyphInfo; /** - * For use in {@link dev.engine_room.flywheel.backend.mixin.FontTextureMixin} + * For use in {@link dev.engine_room.flywheel.impl.mixin.text.FontTextureMixin} * to batch glyph uploads when they're created in a flywheel worker thread. */ public record FontTextureUpload(SheetGlyphInfo info, int x, int y) { diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java similarity index 96% rename from common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java index 11dc3c27a..2d3c63f24 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/BakedGlyphMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.impl.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java similarity index 97% rename from common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java index 0dfcd993c..5c9f245bc 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/CodePointMapMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.backend.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import java.util.function.IntFunction; diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java similarity index 87% rename from common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java index dbea26326..db9cbe0c4 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/FontAccessor.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.impl.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java similarity index 96% rename from common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java index 39b53f211..d31f79a41 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontSetMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.backend.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java similarity index 84% rename from common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java index e8120e5ce..d2723f645 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTexture$NodeAccessor.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.backend.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java similarity index 97% rename from common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java rename to common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java index 37d63df74..358d9e36b 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/mixin/FontTextureMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java @@ -1,4 +1,4 @@ -package dev.engine_room.flywheel.backend.mixin; +package dev.engine_room.flywheel.impl.mixin.text; import java.util.ArrayList; import java.util.List; @@ -20,7 +20,7 @@ import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderSystem; -import dev.engine_room.flywheel.backend.util.FontTextureUpload; +import dev.engine_room.flywheel.impl.FontTextureUpload; import dev.engine_room.flywheel.lib.internal.FontTextureExtension; import dev.engine_room.flywheel.lib.internal.GlyphExtension; import net.minecraft.client.gui.font.FontTexture; diff --git a/common/src/main/resources/flywheel.impl.mixins.json b/common/src/main/resources/flywheel.impl.mixins.json index afae1ba3b..3d4bdb723 100644 --- a/common/src/main/resources/flywheel.impl.mixins.json +++ b/common/src/main/resources/flywheel.impl.mixins.json @@ -5,11 +5,9 @@ "compatibilityLevel": "JAVA_17", "refmap": "flywheel.refmap.json", "client": [ - "BakedGlyphMixin", "BlockEntityTypeMixin", "ClientChunkCacheMixin", "EntityTypeMixin", - "FontAccessor", "LevelMixin", "LevelRendererMixin", "MinecraftMixin", @@ -18,6 +16,12 @@ "PoseStackMixin", "fix.FixFabulousDepthMixin", "fix.FixNormalScalingMixin", + "text.BakedGlyphMixin", + "text.CodePointMapMixin", + "text.FontAccessor", + "text.FontSetMixin", + "text.FontTexture$NodeAccessor", + "text.FontTextureMixin", "visualmanage.BlockEntityMixin", "visualmanage.LevelChunkMixin", "visualmanage.LevelRendererMixin", From bd3aab04bc2b1c1abdbb1382a1349b0d72d08c2e Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Fri, 27 Sep 2024 22:34:14 -0700 Subject: [PATCH 08/14] Can't read the signs - Handle obfuscation in SignVisuals --- .../flywheel/lib/visual/text/TextLayer.java | 2 +- .../flywheel/vanilla/SignVisual.java | 49 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java index 22c27854e..7bc1f8bcc 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java @@ -95,7 +95,7 @@ static GlyphColor defaultTo(int color) { * @return A new GlyphColor. */ static GlyphColor always(int color) { - return style -> adjustColor(color); + return style -> color; } /** diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java index ad03a5f13..531760ef3 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -54,6 +54,10 @@ public class SignVisual extends AbstractBlockEntityVisual imple private final TextVisual[] frontText = new TextVisual[4]; private final TextVisual[] backText = new TextVisual[4]; + // Need to update these every frame, so just remember which ones are obfuscated + // Most of the time this will be empty. + private final List obfuscated = new ArrayList<>(); + private SignText lastFrontText; private SignText lastBackText; @@ -93,22 +97,34 @@ public SignVisual(VisualizationContext ctx, SignBlockEntity blockEntity, float p lastFrontText = blockEntity.getFrontText(); lastBackText = blockEntity.getBackText(); - this.setupText(lastFrontText, frontText, true); - this.setupText(lastBackText, backText, false); + this.setupText(lastFrontText, true); + this.setupText(lastBackText, false); } @Override public void beginFrame(Context ctx) { - // Need to update every frame if the text is obfuscated - + boolean setup = false; if (lastFrontText != blockEntity.getFrontText()) { lastFrontText = blockEntity.getFrontText(); - this.setupText(lastFrontText, frontText, true); + setup = true; } if (lastBackText != blockEntity.getBackText()) { lastBackText = blockEntity.getBackText(); - this.setupText(lastBackText, backText, false); + setup = true; + } + + if (setup) { + // Setup both to make it easier to track obfuscation + obfuscated.clear(); + this.setupText(lastBackText, false); + this.setupText(lastFrontText, true); + } else { + // The is visible check is relatively expensive compared to the boolean checks above, + // so only do it when it'll actually save some work in obfuscating. + if (isVisible(ctx.frustum())) { + obfuscated.forEach(TextVisual::setup); + } } } @@ -158,7 +174,7 @@ public Vec3 getTextOffset() { return TEXT_OFFSET; } - void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { + protected void setupText(SignText text, boolean isFrontText) { FormattedCharSequence[] formattedCharSequences = text.getRenderMessages(Minecraft.getInstance() .isTextFilteringEnabled(), component -> { List list = Minecraft.getInstance().font.split(component, blockEntity.getMaxTextLineWidth()); @@ -167,10 +183,10 @@ void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { List layers = new ArrayList<>(); - int darkColor = adjustColor(getDarkColor(text)); + int darkColor = TextLayer.GlyphColor.adjustColor(getDarkColor(text)); int textColor; if (text.hasGlowingText()) { - textColor = adjustColor(text.getColor() + textColor = TextLayer.GlyphColor.adjustColor(text.getColor() .getTextColor()); layers.add(new SimpleTextLayer.Builder().style(TextLayer.GlyphMeshStyle.OUTLINE) @@ -187,6 +203,8 @@ void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { .bias(1) .build()); + var dst = isFrontText ? frontText : backText; + int lineHeight = blockEntity.getTextLineHeight(); int lineDelta = 4 * lineHeight / 2; for (int m = 0; m < 4; ++m) { @@ -213,17 +231,18 @@ void setupText(SignText text, TextVisual[] dst, boolean isFrontText) { textPose.scale(scale, -scale, scale); textVisual.setup(); + + if (hasObfuscation(formattedCharSequence)) { + this.obfuscated.add(textVisual); + } } } - private static int adjustColor(int color) { - if ((color & 0xFC000000) == 0) { - return color | 0xFF000000; - } - return color; + public static boolean hasObfuscation(FormattedCharSequence text) { + return text.accept((i, s, j) -> s.isObfuscated()); } - static int getDarkColor(SignText signText) { + public static int getDarkColor(SignText signText) { int i = signText.getColor() .getTextColor(); if (i == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) { From 183e2d78b74454b4a89a9f803b6fb682f6cd4c62 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Fri, 27 Sep 2024 23:00:40 -0700 Subject: [PATCH 09/14] Packing up - Save 8 bytes per glyph instance by switching uvs to FloatRepr.NORMALIZED_UNSIGNED_SHORT - With vanilla's 256x glyph atlases we could technically save another 4 bytes, but that would make compat with caxton, which uses 4096x atlases, much more difficult later --- .../flywheel/lib/instance/GlyphInstance.java | 27 ++++++++++--------- .../flywheel/lib/instance/InstanceTypes.java | 18 ++++++------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java index 510afd8bf..6d2c925e8 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -5,6 +5,7 @@ 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 dev.engine_room.flywheel.lib.internal.GlyphExtension; import net.minecraft.client.gui.font.glyphs.BakedGlyph; import net.minecraft.util.FastColor; @@ -15,10 +16,8 @@ public class GlyphInstance extends AbstractInstance { public final Matrix4f pose = new Matrix4f(); - public float u0; - public float u1; - public float v0; - public float v1; + public int us; + public int vs; public byte red = (byte) 0xFF; public byte green = (byte) 0xFF; @@ -34,10 +33,7 @@ public GlyphInstance(InstanceType type, InstanceHandle 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(); + setUvs(glyphReader); float left = glyphReader.flywheel$left(); float right = glyphReader.flywheel$right(); float up = glyphReader.flywheel$up(); @@ -56,10 +52,7 @@ public GlyphInstance setGlyph(BakedGlyph glyph, float x, float y, boolean italic public GlyphInstance setEffect(BakedGlyph glyph, float x0, float y0, float x1, float y1, float depth) { var glyphReader = FlwLibLink.INSTANCE.getGlyphExtension(glyph); - u0 = glyphReader.flywheel$u0(); - u1 = glyphReader.flywheel$u1(); - v0 = glyphReader.flywheel$v0(); - v1 = glyphReader.flywheel$v1(); + setUvs(glyphReader); pose.translate(x0, y0, depth); pose.scale(x1 - x0, y1 - y0, 1.0f); @@ -101,4 +94,14 @@ public GlyphInstance color(byte red, byte green, byte blue) { this.blue = blue; return this; } + + private void setUvs(GlyphExtension glyphReader) { + float u0 = glyphReader.flywheel$u0(); + float u1 = glyphReader.flywheel$u1(); + float v0 = glyphReader.flywheel$v0(); + float v1 = glyphReader.flywheel$v1(); + + us = (int) (u0 * 65536) | ((int) (u1 * 65536) << 16); + vs = (int) (v0 * 65536) | ((int) (v1 * 65536) << 16); + } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index 4212e358f..ec220ce63 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -106,21 +106,19 @@ public final class InstanceTypes { public static final InstanceType GLYPH = SimpleInstanceType.builder(GlyphInstance::new) .layout(LayoutBuilder.create() .matrix("pose", FloatRepr.FLOAT, 4) - .vector("u0u1v0v1", FloatRepr.FLOAT, 4) + .vector("u0u1v0v1", FloatRepr.NORMALIZED_UNSIGNED_SHORT, 4) .vector("color", FloatRepr.NORMALIZED_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.memPutByte(ptr + 80, instance.red); - MemoryUtil.memPutByte(ptr + 81, instance.green); - MemoryUtil.memPutByte(ptr + 82, instance.blue); - MemoryUtil.memPutByte(ptr + 83, instance.alpha); - ExtraMemoryOps.put2x16(ptr + 84, instance.light); + ExtraMemoryOps.put2x16(ptr + 64, instance.us); + ExtraMemoryOps.put2x16(ptr + 68, instance.vs); + MemoryUtil.memPutByte(ptr + 72, instance.red); + MemoryUtil.memPutByte(ptr + 73, instance.green); + MemoryUtil.memPutByte(ptr + 74, instance.blue); + MemoryUtil.memPutByte(ptr + 75, instance.alpha); + ExtraMemoryOps.put2x16(ptr + 76, instance.light); }) .vertexShader(Flywheel.rl("instance/glyph.vert")) .cullShader(Flywheel.rl("instance/cull/glyph.glsl")) From 3dac11899e8c03177fa5315874fad8207143aa7f Mon Sep 17 00:00:00 2001 From: PepperCode1 <44146161+PepperCode1@users.noreply.github.com> Date: Sat, 5 Oct 2024 20:17:13 -0700 Subject: [PATCH 10/14] Improve and polish text utilities --- .../lib/instance/ColoredLitInstance.java | 11 +- .../instance/ColoredLitOverlayInstance.java | 18 + .../flywheel/lib/instance/GlyphInstance.java | 97 ++--- .../flywheel/lib/instance/InstanceTypes.java | 4 +- .../lib/instance/OrientedInstance.java | 2 +- .../flywheel/lib/instance/PosedInstance.java | 2 +- .../lib/instance/TransformedInstance.java | 2 +- ...xtension.java => BakedGlyphExtension.java} | 2 +- .../flywheel/lib/internal/FlwLibLink.java | 6 +- .../lib/internal/FontTextureExtension.java | 7 - .../flywheel/lib/material/LightShaders.java | 2 +- .../flywheel/lib/math/DataPacker.java | 28 ++ .../lib/visual/component/FireComponent.java | 2 +- .../lib/visual/text/SimpleTextLayer.java | 47 +-- .../flywheel/lib/visual/text/TextLayer.java | 222 +++++++----- .../flywheel/lib/visual/text/TextLayers.java | 45 +++ .../flywheel/lib/visual/text/TextVisual.java | 338 +++++++++++------- .../flywheel/flywheel/instance/glyph.vert | 2 +- .../flywheel/impl/FlwLibLinkImpl.java | 15 +- .../impl/extension/FontTextureExtension.java | 7 + .../impl/mixin/text/BakedGlyphMixin.java | 4 +- .../impl/mixin/text/FontAccessor.java | 4 + .../impl/mixin/text/FontSetMixin.java | 2 +- .../impl/mixin/text/FontTextureMixin.java | 10 +- .../flywheel/vanilla/SignVisual.java | 178 ++++----- gradle.properties | 2 +- 26 files changed, 611 insertions(+), 448 deletions(-) create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java rename common/src/lib/java/dev/engine_room/flywheel/lib/internal/{GlyphExtension.java => BakedGlyphExtension.java} (90%) delete mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java create mode 100644 common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java create mode 100644 common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java index 6d2f0ed66..e8f95baf5 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java @@ -2,7 +2,6 @@ import dev.engine_room.flywheel.api.instance.InstanceHandle; import dev.engine_room.flywheel.api.instance.InstanceType; -import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.util.FastColor; public abstract class ColoredLitInstance extends AbstractInstance implements FlatLit { @@ -11,7 +10,6 @@ public abstract class ColoredLitInstance extends AbstractInstance implements Fla public byte blue = (byte) 0xFF; public byte alpha = (byte) 0xFF; - public int overlay = OverlayTexture.NO_OVERLAY; public int light = 0; public ColoredLitInstance(InstanceType type, InstanceHandle handle) { @@ -49,9 +47,12 @@ public ColoredLitInstance color(byte red, byte green, byte blue) { return this; } - public ColoredLitInstance overlay(int overlay) { - this.overlay = overlay; - return this; + public ColoredLitInstance color(float red, float green, float blue, float alpha) { + return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f), (byte) (alpha * 255f)); + } + + public ColoredLitInstance color(float red, float green, float blue) { + return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f)); } @Override diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java new file mode 100644 index 000000000..ab0725d9b --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java @@ -0,0 +1,18 @@ +package dev.engine_room.flywheel.lib.instance; + +import dev.engine_room.flywheel.api.instance.InstanceHandle; +import dev.engine_room.flywheel.api.instance.InstanceType; +import net.minecraft.client.renderer.texture.OverlayTexture; + +public abstract class ColoredLitOverlayInstance extends ColoredLitInstance { + public int overlay = OverlayTexture.NO_OVERLAY; + + public ColoredLitOverlayInstance(InstanceType type, InstanceHandle handle) { + super(type, handle); + } + + public ColoredLitOverlayInstance overlay(int overlay) { + this.overlay = overlay; + return this; + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java index 6d2c925e8..c5e999d9f 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -1,107 +1,66 @@ package dev.engine_room.flywheel.lib.instance; import org.joml.Matrix4f; +import org.joml.Matrix4fc; import dev.engine_room.flywheel.api.instance.InstanceHandle; import dev.engine_room.flywheel.api.instance.InstanceType; +import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension; import dev.engine_room.flywheel.lib.internal.FlwLibLink; -import dev.engine_room.flywheel.lib.internal.GlyphExtension; +import dev.engine_room.flywheel.lib.math.DataPacker; import net.minecraft.client.gui.font.glyphs.BakedGlyph; -import net.minecraft.util.FastColor; -public class GlyphInstance extends AbstractInstance { +public class GlyphInstance extends ColoredLitInstance { // Skew x by 1 - 0.25 * y // Note that columns are written as rows. - private static final Matrix4f ITALIC_SKEW = new Matrix4f(1, 0, 0, 0, -0.25f, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1); + private static final Matrix4fc ITALIC_SKEW = new Matrix4f(1, 0, 0, 0, -0.25f, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1); public final Matrix4f pose = new Matrix4f(); - public int us; - public int vs; + public int packedUs; + public int packedVs; - 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) { + 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); + public GlyphInstance setGlyph(BakedGlyph glyph, Matrix4fc initialPose, float x, float y, boolean italic) { + var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); + setUvs(glyphExtension); - setUvs(glyphReader); - float left = glyphReader.flywheel$left(); - float right = glyphReader.flywheel$right(); - float up = glyphReader.flywheel$up(); - float down = glyphReader.flywheel$down(); + float left = glyphExtension.flywheel$left(); + float up = glyphExtension.flywheel$up(); - pose.translate(x + left, y + up - 3.0f, 0.0f); - pose.scale(right - left, down - up, 1.0f); + pose.set(initialPose); + pose.translate(x, y, 0.0f); if (italic) { pose.mul(ITALIC_SKEW); } + pose.translate(left, up - 3.0f, 0.0f); + return this; } - public GlyphInstance setEffect(BakedGlyph glyph, float x0, float y0, float x1, float y1, float depth) { - var glyphReader = FlwLibLink.INSTANCE.getGlyphExtension(glyph); - - setUvs(glyphReader); + public GlyphInstance setEffect(BakedGlyph glyph, Matrix4fc initialPose, float x0, float y0, float x1, float y1, float depth) { + var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); + setUvs(glyphExtension); + pose.set(initialPose); pose.translate(x0, y0, depth); pose.scale(x1 - x0, y1 - y0, 1.0f); 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(float red, float green, float blue, float alpha) { - return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f), (byte) (alpha * 255f)); - } - - 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; - } - - private void setUvs(GlyphExtension glyphReader) { - float u0 = glyphReader.flywheel$u0(); - float u1 = glyphReader.flywheel$u1(); - float v0 = glyphReader.flywheel$v0(); - float v1 = glyphReader.flywheel$v1(); + private void setUvs(BakedGlyphExtension glyphExtension) { + float u0 = glyphExtension.flywheel$u0(); + float u1 = glyphExtension.flywheel$u1(); + float v0 = glyphExtension.flywheel$v0(); + float v1 = glyphExtension.flywheel$v1(); - us = (int) (u0 * 65536) | ((int) (u1 * 65536) << 16); - vs = (int) (v0 * 65536) | ((int) (v1 * 65536) << 16); + packedUs = ((int) DataPacker.packNormU16(u1) << 16) | (int) DataPacker.packNormU16(u0); + packedVs = ((int) DataPacker.packNormU16(v1) << 16) | (int) DataPacker.packNormU16(v0); } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index ec220ce63..c47cfadcb 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -112,8 +112,8 @@ public final class InstanceTypes { .build()) .writer((ptr, instance) -> { ExtraMemoryOps.putMatrix4f(ptr, instance.pose); - ExtraMemoryOps.put2x16(ptr + 64, instance.us); - ExtraMemoryOps.put2x16(ptr + 68, instance.vs); + ExtraMemoryOps.put2x16(ptr + 64, instance.packedUs); + ExtraMemoryOps.put2x16(ptr + 68, instance.packedVs); MemoryUtil.memPutByte(ptr + 72, instance.red); MemoryUtil.memPutByte(ptr + 73, instance.green); MemoryUtil.memPutByte(ptr + 74, instance.blue); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java index 321caf936..db59429e2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java @@ -10,7 +10,7 @@ import net.minecraft.core.Vec3i; import net.minecraft.world.phys.Vec3; -public class OrientedInstance extends ColoredLitInstance implements Rotate { +public class OrientedInstance extends ColoredLitOverlayInstance implements Rotate { public float posX; public float posY; public float posZ; diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java index 43e6df47b..f00a6ac5d 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java @@ -13,7 +13,7 @@ import dev.engine_room.flywheel.lib.transform.Transform; import net.minecraft.util.Mth; -public class PosedInstance extends ColoredLitInstance implements Transform { +public class PosedInstance extends ColoredLitOverlayInstance implements Transform { public final Matrix4f pose = new Matrix4f(); public final Matrix3f normal = new Matrix3f(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java index 8bab28975..97ad11337 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java @@ -10,7 +10,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType; import dev.engine_room.flywheel.lib.transform.Affine; -public class TransformedInstance extends ColoredLitInstance implements Affine { +public class TransformedInstance extends ColoredLitOverlayInstance implements Affine { public final Matrix4f pose = new Matrix4f(); public TransformedInstance(InstanceType type, InstanceHandle handle) { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java similarity index 90% rename from common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java rename to common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java index 6d62472e1..5cfa0e45e 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/GlyphExtension.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java @@ -2,7 +2,7 @@ import net.minecraft.resources.ResourceLocation; -public interface GlyphExtension { +public interface BakedGlyphExtension { float flywheel$u0(); float flywheel$u1(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java index 4899298c6..0920cf104 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java @@ -29,7 +29,9 @@ public interface FlwLibLink { Deque getPoseStack(PoseStack stack); - GlyphExtension getGlyphExtension(BakedGlyph glyph); - FontSet getFontSet(Font font, ResourceLocation loc); + + boolean getFilterFishyGlyphs(Font font); + + BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph); } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java deleted file mode 100644 index 8b7b5cce7..000000000 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FontTextureExtension.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.engine_room.flywheel.lib.internal; - -import net.minecraft.resources.ResourceLocation; - -public interface FontTextureExtension { - void flywheel$setName(ResourceLocation value); -} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java b/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java index 3e15c76ca..ac9e351cc 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java @@ -3,7 +3,7 @@ import dev.engine_room.flywheel.api.Flywheel; import dev.engine_room.flywheel.api.material.LightShader; -public class LightShaders { +public final class LightShaders { public static final LightShader SMOOTH_WHEN_EMBEDDED = new SimpleLightShader(Flywheel.rl("light/smooth_when_embedded.glsl")); public static final LightShader SMOOTH = new SimpleLightShader(Flywheel.rl("light/smooth.glsl")); public static final LightShader FLAT = new SimpleLightShader(Flywheel.rl("light/flat.glsl")); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java index 6fa5f33fe..363888f1a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java @@ -33,4 +33,32 @@ public static byte packNormI8(float f) { public static float unpackNormI8(byte b) { return (float) b / 127f; } + + /** + * Pack a float as an unsigned, normalized short. + */ + public static short packNormU16(float f) { + return (short) (int) (Mth.clamp(f, 0.0f, 1.0f) * 65535); + } + + /** + * Unpack an unsigned, normalized short to a float. + */ + public static float unpackNormU16(short s) { + return (float) (Short.toUnsignedInt(s)) / 65535f; + } + + /** + * Pack a float as a signed, normalized byte. + */ + public static short packNormI16(float f) { + return (short) (Mth.clamp(f, -1.0f, 1.0f) * 32767); + } + + /** + * Unpack a signed, normalized byte to a float. + */ + public static float unpackNormI16(short s) { + return (float) s / 32767f; + } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java index 0e0cb8301..e9032b931 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java @@ -125,7 +125,7 @@ public void delete() { } private record FireMesh(TextureAtlasSprite sprite) implements QuadMesh { - private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, (float) (Math.sqrt(2) * 0.5)); + private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f); @Override public int vertexCount() { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java index 8b2943929..39bdd1dd5 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java @@ -3,25 +3,24 @@ import java.util.Objects; import org.jetbrains.annotations.Nullable; +import org.joml.Vector2f; +import org.joml.Vector2fc; -public record SimpleTextLayer(GlyphMeshStyle style, GlyphMaterial material, GlyphColor color, int bias, float offsetX, - float offsetY, float effectOffsetX, float effectOffsetY) implements TextLayer { +public record SimpleTextLayer(GlyphPattern pattern, GlyphMaterial material, GlyphColor color, Vector2fc offset, int bias) implements TextLayer { public static class Builder { + private static final Vector2fc NO_OFFSET = new Vector2f(); + @Nullable - private GlyphMeshStyle style; + private GlyphPattern pattern; @Nullable private GlyphMaterial material; @Nullable private GlyphColor color; + private Vector2fc offset = NO_OFFSET; + private int bias = 0; - private int bias; - private float offsetX = 0; - private float offsetY = 0; - private float effectOffsetX = 1; - private float effectOffsetY = 1; - - public Builder style(GlyphMeshStyle style) { - this.style = style; + public Builder pattern(GlyphPattern pattern) { + this.pattern = pattern; return this; } @@ -35,37 +34,27 @@ public Builder color(GlyphColor color) { return this; } - public Builder bias(int bias) { - this.bias = bias; - return this; - } - - public Builder offsetX(float offsetX) { - this.offsetX = offsetX; - return this; - } - - public Builder offsetY(float offsetY) { - this.offsetY = offsetY; + public Builder offset(Vector2fc offset) { + this.offset = offset; return this; } - public Builder effectOffsetX(float effectOffsetX) { - this.effectOffsetX = effectOffsetX; + public Builder offset(float offsetX, float offsetY) { + offset = new Vector2f(offsetX, offsetY); return this; } - public Builder effectOffsetY(float effectOffsetY) { - this.effectOffsetY = effectOffsetY; + public Builder bias(int bias) { + this.bias = bias; return this; } public SimpleTextLayer build() { - Objects.requireNonNull(style); + Objects.requireNonNull(pattern); Objects.requireNonNull(material); Objects.requireNonNull(color); - return new SimpleTextLayer(style, material, color, bias, offsetX, offsetY, effectOffsetX, effectOffsetY); + return new SimpleTextLayer(pattern, material, color, offset, bias); } } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java index 7bc1f8bcc..35151c5d2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java @@ -2,24 +2,29 @@ import java.util.function.Consumer; -import org.joml.Vector3f; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector2f; +import org.joml.Vector2fc; +import dev.engine_room.flywheel.api.material.DepthTest; import dev.engine_room.flywheel.api.material.Material; +import dev.engine_room.flywheel.api.material.Transparency; +import dev.engine_room.flywheel.api.material.WriteMask; import dev.engine_room.flywheel.lib.material.CutoutShaders; +import dev.engine_room.flywheel.lib.material.FogShaders; import dev.engine_room.flywheel.lib.material.SimpleMaterial; -import net.minecraft.network.chat.Style; +import net.minecraft.client.gui.Font; import net.minecraft.network.chat.TextColor; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; public interface TextLayer { - float ONE_PIXEL = 0.125f; - /** - * The style of individual glyphs. + * The pattern of individual glyphs. * - * @return A GlyphMeshStyle. + * @return A GlyphPattern. */ - GlyphMeshStyle style(); + GlyphPattern pattern(); /** * A mapping from texture ResourceLocations to Flywheel materials. @@ -36,105 +41,90 @@ public interface TextLayer { GlyphColor color(); /** - * The instancer bias for this layer. - * - * @return The bias. - */ - int bias(); - - /** - * The x offset of text content in this layer. + * The offset of text in this layer. * - * @return The x offset. + * @return The offset. */ - float offsetX(); + Vector2fc offset(); /** - * The y offset of text content in this layer. - * - * @return The y offset. - */ - float offsetY(); - - /** - * The x offset of text effects such as strikethrough or underline in this layer. - * - * @return The x offset. - */ - float effectOffsetX(); - - /** - * The y offset of text effects such as strikethrough or underline in this layer. + * The instancer bias for this layer. * - * @return The y offset. + * @return The bias. */ - float effectOffsetY(); + int bias(); + // TODO: probably just convert this to Iterable @FunctionalInterface - interface GlyphColor { + interface GlyphPattern { /** - * Default to the given color if no color is specified in the style. - * - * @param color The ARGB color to default to. - * @return A new GlyphColor. + * The pattern for a single glyph with no offset. */ - static GlyphColor defaultTo(int color) { - return style -> { - TextColor textColor = style.getColor(); - if (textColor != null) { - return adjustColor(textColor.getValue()); - } - return color; - }; - } + GlyphPattern SINGLE = out -> out.accept(new Vector2f(0, 0)); /** - * Always use the given color, regardless of the style. - * - * @param color The ARGB color to use. - * @return A new GlyphColor. + * The pattern for an 8x outline as used by glowing text on signs. */ - static GlyphColor always(int color) { - return style -> color; - } + GlyphPattern OUTLINE = out -> { + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + if (x == 0 && y == 0) { + continue; + } - /** - * Adjust the color to be fully opaque if it's very close to having 0 alpha. - * - * @param color The ARGB color to adjust. - * @return The adjusted color. - */ - static int adjustColor(int color) { - if ((color & 0xFC000000) == 0) { - return color | 0xFF000000; + out.accept(new Vector2f(x, y)); + } } - return color; - } + }; /** - * Convert a style to a color. + * Add an arbitrary amount of glyphs. Each accepted vector represents + * the offset of a new glyph quad. * - * @param style The style of the text to colorize. - * @return The color to use, in ARGB format. + * @param out The consumer to accept the offset of a new glyph quad */ - int color(Style style); + void addGlyphs(Consumer out); } @FunctionalInterface interface GlyphMaterial { - GlyphMaterial SIMPLE = texture -> SimpleMaterial.builder() + // FIXME: account for intensity + GlyphMaterial NORMAL = texture -> SimpleMaterial.builder() + .cutout(CutoutShaders.ONE_TENTH) .texture(texture) + .mipmap(false) + .transparency(Transparency.TRANSLUCENT) + .diffuse(false) + .build(); + + GlyphMaterial SEE_THROUGH = texture -> SimpleMaterial.builder() + .fog(FogShaders.NONE) .cutout(CutoutShaders.ONE_TENTH) + .texture(texture) + .mipmap(false) + .depthTest(DepthTest.ALWAYS) + .transparency(Transparency.TRANSLUCENT) + .writeMask(WriteMask.COLOR) .diffuse(false) .build(); GlyphMaterial POLYGON_OFFSET = texture -> SimpleMaterial.builder() - .texture(texture) .cutout(CutoutShaders.ONE_TENTH) - .diffuse(false) + .texture(texture) + .mipmap(false) .polygonOffset(true) + .transparency(Transparency.TRANSLUCENT) + .diffuse(false) .build(); + static GlyphMaterial fromDisplayMode(Font.DisplayMode displayMode) { + return switch (displayMode) { + case NORMAL -> NORMAL; + case SEE_THROUGH -> SEE_THROUGH; + case POLYGON_OFFSET -> POLYGON_OFFSET; + }; + } + /** * Create a Flywheel material for the given glyph texture. * @@ -145,35 +135,83 @@ interface GlyphMaterial { } @FunctionalInterface - interface GlyphMeshStyle { + interface GlyphColor { /** - * The standard style for glyphs with no repetition. + * Default to the given color if no color is specified in the style. + * + * @param color The ARGB color to default to. + * @return A new GlyphColor. */ - GlyphMeshStyle SIMPLE = out -> out.accept(new Vector3f(0, 0, 0)); + static GlyphColor defaultTo(int color, float dimFactor) { + int finalColor; + if (dimFactor != 1.0f) { + finalColor = FastColor.ARGB32.color( + FastColor.ARGB32.alpha(color), + (int) (FastColor.ARGB32.red(color) * dimFactor), + (int) (FastColor.ARGB32.green(color) * dimFactor), + (int) (FastColor.ARGB32.blue(color) * dimFactor) + ); + } else { + finalColor = color; + } + + return textColor -> { + if (textColor != null) { + int textColorArgb = textColor.getValue(); + if (dimFactor != 1.0f) { + return FastColor.ARGB32.color( + FastColor.ARGB32.alpha(finalColor), + (int) (FastColor.ARGB32.red(textColorArgb) * dimFactor), + (int) (FastColor.ARGB32.green(textColorArgb) * dimFactor), + (int) (FastColor.ARGB32.blue(textColorArgb) * dimFactor) + ); + } else { + return (finalColor & 0xFF000000) | (textColorArgb & 0xFFFFFF); + } + } + return finalColor; + }; + } /** - * The style for glyphs with a 8x outline as used by glowing text on signs. + * Default to the given color if no color is specified in the style. + * + * @param color The ARGB color to default to. + * @return A new GlyphColor. */ - GlyphMeshStyle OUTLINE = out -> { - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - if (x == 0 && y == 0) { - continue; - } + static GlyphColor defaultTo(int color) { + return defaultTo(color, 1.0f); + } - out.accept(new Vector3f(x * ONE_PIXEL, y * ONE_PIXEL, 0)); - } - } - }; + /** + * Always use the given color, regardless of the style. + * + * @param color The ARGB color to use. + * @return A new GlyphColor. + */ + static GlyphColor always(int color) { + return textColor -> color; + } /** - * Add quads to the mesh. Each vec3 submitted to out will be expanded - * into a unit quad in the XY plane with the lowest corner at the given vec3. - * You can think of each submitted vec3 as a duplication of a glyph. + * Adjust the color to be fully opaque if it's very close to having 0 alpha. * - * @param out The consumer to accept the quads + * @param color The ARGB color to adjust. + * @return The adjusted color. */ - void addQuads(Consumer out); + static int adjustColor(int color) { + if ((color & 0xFC000000) == 0) { + return color | 0xFF000000; + } + return color; + } + /** + * Convert a nullable text color to a color. + * + * @param textColor The color of the text to colorize. + * @return The color to use, in ARGB format. + */ + int color(@Nullable TextColor textColor); } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java new file mode 100644 index 000000000..57701ce76 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java @@ -0,0 +1,45 @@ +package dev.engine_room.flywheel.lib.visual.text; + +import net.minecraft.client.gui.Font; + +public final class TextLayers { + public static TextLayer normal(int color, Font.DisplayMode displayMode, int bias) { + return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE) + .material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode)) + .color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color))) + .bias(bias) + .build(); + } + + public static TextLayer normal(int color, Font.DisplayMode displayMode) { + return normal(color, displayMode, 0); + } + + public static TextLayer dropShadow(int color, Font.DisplayMode displayMode, int bias) { + return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE) + .material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode)) + .color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color), 0.25f)) + .offset(1, 1) + .bias(bias) + .build(); + } + + public static TextLayer dropShadow(int color, Font.DisplayMode displayMode) { + return dropShadow(color, displayMode, 0); + } + + public static TextLayer outline(int color, int bias) { + return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.OUTLINE) + .material(TextLayer.GlyphMaterial.NORMAL) + .color(TextLayer.GlyphColor.always(TextLayer.GlyphColor.adjustColor(color))) + .bias(bias) + .build(); + } + + public static TextLayer outline(int color) { + return outline(color, 0); + } + + private TextLayers() { + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java index 32c6943bc..677a60ab2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -4,15 +4,18 @@ import java.util.Collection; import java.util.List; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.joml.Matrix4f; -import org.joml.Vector3f; +import org.joml.Vector2f; +import org.joml.Vector2fc; import org.joml.Vector4f; import org.joml.Vector4fc; import com.mojang.blaze3d.font.GlyphInfo; import dev.engine_room.flywheel.api.instance.InstancerProvider; +import dev.engine_room.flywheel.api.model.Mesh; import dev.engine_room.flywheel.api.model.Model; import dev.engine_room.flywheel.api.vertex.MutableVertexList; import dev.engine_room.flywheel.lib.instance.GlyphInstance; @@ -27,65 +30,66 @@ 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.client.renderer.texture.OverlayTexture; import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.FormattedCharSink; +import net.minecraft.util.Mth; /** * A visual that renders a single line of text. */ -public class TextVisual { +public final class TextVisual { + private static final Font FONT = Minecraft.getInstance().font; + + private static final ResourceReloadCache GLYPH_MESH_CACHE = new ResourceReloadCache<>(GlyphMeshKey::into); + private static final ResourceReloadCache GLYPH_MODEL_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); + private static final ThreadLocal SINKS = ThreadLocal.withInitial(Sink::new); - private final SmartRecycler recycler; + private final SmartRecycler recycler; + private final List layers = new ArrayList<>(); + private final Matrix4f pose = new Matrix4f(); - private FormattedCharSequence content = FormattedCharSequence.EMPTY; + private FormattedCharSequence text = FormattedCharSequence.EMPTY; private float x; private float y; private int backgroundColor = 0; private int light; - private boolean fullBright; - - private final List layers = new ArrayList<>(); - - private final Matrix4f pose = new Matrix4f(); public TextVisual(InstancerProvider provider) { - recycler = new SmartRecycler<>(key -> provider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key.modelKey), key.bias) + recycler = new SmartRecycler<>(key -> provider.instancer(InstanceTypes.GLYPH, GLYPH_MODEL_CACHE.get(key.modelKey), key.bias) .createInstance()); } - public TextVisual content(FormattedCharSequence content) { - this.content = content; + public TextVisual addLayer(TextLayer layer) { + layers.add(layer); return this; } - public Matrix4f pose() { - return pose; + public TextVisual addLayers(Collection layers) { + this.layers.addAll(layers); + return this; } - public TextVisual clearLayers() { - layers.clear(); + public TextVisual layers(Collection layers) { + this.layers.clear(); + this.layers.addAll(layers); return this; } - public TextVisual addLayer(TextLayer layer) { - layers.add(layer); + public TextVisual clearLayers() { + layers.clear(); return this; } - public TextVisual layers(Collection layers) { - this.layers.clear(); - this.layers.addAll(layers); - return this; + public Matrix4f pose() { + return pose; } - public TextVisual pos(float x, float y) { - this.x = x; - this.y = y; + public TextVisual text(FormattedCharSequence text) { + this.text = text; return this; } @@ -99,6 +103,12 @@ public TextVisual y(float y) { return this; } + public TextVisual pos(float x, float y) { + this.x = x; + this.y = y; + return this; + } + public TextVisual backgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; return this; @@ -109,33 +119,35 @@ public TextVisual light(int light) { return this; } - public TextVisual fullBright(boolean fullBright) { - this.fullBright = fullBright; + public TextVisual reset() { + layers.clear(); + pose.identity(); + + text = FormattedCharSequence.EMPTY; + x = 0; + y = 0; + backgroundColor = 0; + light = 0; + return this; } - // TODO: method to just update pose or light without recalculating text + // TODO: track glyph instances and add method to update only UVs of obfuscated glyphs, method to update only + // background color, and method to only update light public void setup() { recycler.resetCount(); var sink = SINKS.get(); - - var light = fullBright ? LightTexture.FULL_BRIGHT : this.light; sink.prepare(recycler, pose, light); int maxX = 0; - // Can we flip the inner and outer loops here? - // Would that even be better? for (TextLayer layer : layers) { - sink.x = x; - sink.y = y; - sink.layer = layer; - content.accept(sink); + sink.prepareForLayer(layer, x, y); + text.accept(sink); maxX = Math.max(maxX, (int) sink.x); } sink.addBackground(backgroundColor, x, maxX); - sink.clear(); recycler.discardExtra(); @@ -145,166 +157,226 @@ public void delete() { recycler.delete(); } - private static class Sink implements FormattedCharSink { - private final Font font; + private record GlyphMeshKey(float glyphWidth, float glyphHeight, TextLayer.GlyphPattern pattern, boolean bold, float boldOffset, float shadowOffset) { + public GlyphMesh into() { + List out = new ArrayList<>(); + + pattern.addGlyphs(offsetc -> { + Vector2f offset = new Vector2f(offsetc).mul(shadowOffset); + out.add(offset); + + if (bold) { + out.add(new Vector2f(offset.x() + boldOffset, offset.y())); + } + }); + + return new GlyphMesh(glyphWidth, glyphHeight, out.toArray(Vector2fc[]::new)); + } + } + + private record GlyphModelKey(@Nullable GlyphMeshKey meshKey, TextLayer.GlyphMaterial material, ResourceLocation texture) { + public Model into() { + Mesh mesh; + + if (meshKey != null) { + mesh = GLYPH_MESH_CACHE.get(meshKey); + } else { + mesh = GlyphEffectMesh.INSTANCE; + } + + return new SingleMeshModel(mesh, material.create(texture)); + } + } + + private record GlyphInstanceKey(GlyphModelKey modelKey, int bias) { + } + private static class Sink implements FormattedCharSink { @UnknownNullability - private SmartRecycler recycler; + private SmartRecycler recycler; @UnknownNullability private Matrix4f pose; - @UnknownNullability - private TextLayer layer; - private int light; + @UnknownNullability + private TextLayer layer; private float x; private float y; - private Sink() { - font = Minecraft.getInstance().font; - } - - private void prepare(SmartRecycler recycler, Matrix4f pose, int light) { + public void prepare(SmartRecycler recycler, Matrix4f pose, int light) { this.recycler = recycler; this.pose = pose; this.light = light; } - private void clear() { + public void prepareForLayer(TextLayer layer, float x, float y) { + this.layer = layer; + this.x = x; + this.y = y; + } + + public void clear() { recycler = null; pose = null; layer = null; } @Override - public boolean accept(int i, Style style, int j) { - 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 bold = style.isBold(); + public boolean accept(int index, Style style, int codePoint) { + FontSet fontSet = FlwLibLink.INSTANCE.getFontSet(FONT, style.getFont()); + GlyphInfo glyphInfo = fontSet.getGlyphInfo(codePoint, FlwLibLink.INSTANCE.getFilterFishyGlyphs(FONT)); + BakedGlyph glyph = style.isObfuscated() && codePoint != ' ' ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(codePoint); + boolean bold = style.isBold(); int color = layer.color() - .color(style); - - if (!(bakedGlyph instanceof EmptyGlyph)) { - var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); - - GlyphInstance glyph = recycler.get(key(glyphExtension.flywheel$texture(), bold, layer.style())); - - glyph.pose.set(pose); - glyph.setGlyph(bakedGlyph, this.x + layer.offsetX(), this.y + layer.offsetY(), style.isItalic()); - glyph.colorArgb(color); - glyph.light = light; - glyph.setChanged(); + .color(style.getColor()); + Vector2fc offset = layer.offset(); + + if (!(glyph instanceof EmptyGlyph)) { + GlyphInstance instance = recycler.get(key(glyphInfo, glyph, layer.pattern(), bold)); + float shadowOffset = glyphInfo.getShadowOffset(); + instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, y + offset.y() * shadowOffset, style.isItalic()); + instance.colorArgb(color); + instance.light(light); + instance.setChanged(); } + float advance = glyphInfo.getAdvance(bold); - float effectX = layer.effectOffsetX(); - float effectY = layer.effectOffsetY(); + // SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer. + float effectOffsetX = offset.x(); + float effectOffsetY = offset.y(); if (style.isStrikethrough()) { - this.addEffect(this.x + effectX - 1.0f, this.y + effectY + 4.5f, this.x + effectX + advance, this.y + effectY + 4.5f - 1.0f, 0.01f, color); + addEffect(x + effectOffsetX - 1.0f, y + effectOffsetY + 4.5f, x + effectOffsetX + advance, y + effectOffsetY + 4.5f - 1.0f, 0.01f, color); } if (style.isUnderlined()) { - this.addEffect(this.x + effectX - 1.0f, this.y + effectY + 9.0f, this.x + effectX + advance, this.y + effectY + 9.0f - 1.0f, 0.01f, color); + addEffect(x + effectOffsetX - 1.0f, y + effectOffsetY + 9.0f, x + effectOffsetX + advance, y + effectOffsetY + 9.0f - 1.0f, 0.01f, color); } - this.x += advance; + + x += advance; return true; } + public void addBackground(int backgroundColor, float startX, float endX) { + if (backgroundColor != 0) { + addEffect(startX - 1.0f, y + 9.0f, endX + 1.0f, y - 1.0f, 0.01f, backgroundColor); + } + } + private void addEffect(float x0, float y0, float x1, float y1, float depth, int colorArgb) { - BakedGlyph bakedGlyph = FlwLibLink.INSTANCE.getFontSet(font, Style.DEFAULT_FONT) + BakedGlyph glyph = FlwLibLink.INSTANCE.getFontSet(FONT, Style.DEFAULT_FONT) .whiteGlyph(); - var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph); + GlyphInstance instance = recycler.get(effectKey(glyph)); + instance.setEffect(glyph, pose, x0, y0, x1, y1, depth); + instance.colorArgb(colorArgb); + instance.light(light); + instance.setChanged(); + } - GlyphInstance glyph = recycler.get(key(glyphExtension.flywheel$texture(), false, TextLayer.GlyphMeshStyle.SIMPLE)); + private GlyphInstanceKey key(GlyphInfo glyphInfo, BakedGlyph glyph, TextLayer.GlyphPattern pattern, boolean bold) { + var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); + float glyphWidth = glyphExtension.flywheel$right() - glyphExtension.flywheel$left(); + float glyphHeight = glyphExtension.flywheel$down() - glyphExtension.flywheel$up(); - glyph.pose.set(pose); - glyph.setEffect(bakedGlyph, x0, y0, x1, y1, depth); - glyph.colorArgb(colorArgb); - glyph.light = light; - glyph.setChanged(); + return key(glyphWidth, glyphHeight, glyphExtension.flywheel$texture(), pattern, bold, bold ? glyphInfo.getBoldOffset() : 0, glyphInfo.getShadowOffset()); } - public void addBackground(int backgroundColor, float startX, float endX) { - if (backgroundColor != 0) { - this.addEffect(startX - 1.0f, this.y + 9.0f, endX + 1.0f, this.y - 1.0f, 0.01f, backgroundColor); - } + private GlyphInstanceKey key(float glyphWidth, float glyphHeight, ResourceLocation texture, TextLayer.GlyphPattern pattern, boolean bold, float boldOffset, float shadowOffset) { + var meshKey = new GlyphMeshKey(glyphWidth, glyphHeight, pattern, bold, boldOffset, shadowOffset); + var modelKey = new GlyphModelKey(meshKey, layer.material(), texture); + return new GlyphInstanceKey(modelKey, layer.bias()); } - private GlyphInstancerKey key(ResourceLocation texture, boolean bold, TextLayer.GlyphMeshStyle style) { - var meshKey = new GlyphMeshKey(style, bold); - var modelKey = new GlyphModelKey(texture, meshKey, layer.material()); - return new GlyphInstancerKey(modelKey, layer.bias()); + private GlyphInstanceKey effectKey(BakedGlyph glyph) { + var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); + return effectKey(glyphExtension.flywheel$texture()); } - } - private record GlyphInstancerKey(GlyphModelKey modelKey, int bias) { + private GlyphInstanceKey effectKey(ResourceLocation texture) { + var modelKey = new GlyphModelKey(null, layer.material(), texture); + return new GlyphInstanceKey(modelKey, layer.bias()); + } } - private static final ResourceReloadCache GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); - private static final ResourceReloadCache MESH_CACHE = new ResourceReloadCache<>(GlyphMeshKey::into); + /** + * A mesh that represents a pattern of a glyph with a certain width and height. Expects to be drawn with the glyph + * instance type. + * + * @param offsets Each offset will be expanded into a glyph quad. + */ + private record GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets, Vector4fc boundingSphere) implements QuadMesh { + private static final float[] X = new float[] { 0, 0, 1, 1 }; + private static final float[] Y = new float[] { 0, 1, 1, 0 }; - private record GlyphModelKey(ResourceLocation font, GlyphMeshKey meshKey, TextLayer.GlyphMaterial material) { - private Model into() { - return new SingleMeshModel(MESH_CACHE.get(meshKey), material.create(font)); + // FIXME: what is the actual bounding sphere?? + public GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets) { + this(glyphWidth, glyphHeight, offsets, new Vector4f(0, 0, 0, Math.max(glyphWidth, glyphHeight) * 2 * Mth.SQRT_OF_TWO)); } - } - private record GlyphMeshKey(TextLayer.GlyphMeshStyle style, boolean bold) { - public GlyphMesh into() { - List out = new ArrayList<>(); + @Override + public int vertexCount() { + return 4 * offsets.length; + } - style.addQuads(quad -> { - out.add(quad); - if (bold) { - out.add(new Vector3f(quad.x + TextLayer.ONE_PIXEL, quad.y, quad.z)); + @Override + public void write(MutableVertexList vertexList) { + for (int i = 0; i < offsets.length; i++) { + Vector2fc offset = offsets[i]; + var startVertex = i * 4; + + for (int j = 0; j < 4; j++) { + vertexList.x(startVertex + j, offset.x() + (glyphWidth * X[j])); + vertexList.y(startVertex + j, offset.y() + (glyphHeight * Y[j])); + vertexList.z(startVertex + j, 0); + vertexList.r(startVertex + j, 1); + vertexList.g(startVertex + j, 1); + vertexList.b(startVertex + j, 1); + vertexList.a(startVertex + j, 1); + vertexList.overlay(startVertex + j, OverlayTexture.NO_OVERLAY); + vertexList.normalX(startVertex + j, 0); + vertexList.normalY(startVertex + j, 0); + vertexList.normalZ(startVertex + j, 1); } - }); + } + } - return new GlyphMesh(out.toArray(new Vector3f[0])); + @Override + public Vector4fc boundingSphere() { + return boundingSphere; } } - /** - * A mesh that represents a single glyph. Expects to be drawn with the glyph instance type. - * - * @param quads Each quad will be expanded into 4 vertices. - */ - private record GlyphMesh(Vector3f[] quads) implements QuadMesh { - private static final float[] X = new float[]{0, 0, 1, 1}; - private static final float[] Y = new float[]{0, 1, 1, 0}; + private record GlyphEffectMesh() implements QuadMesh { + private static final float[] X = new float[] { 0, 1, 1, 0 }; + private static final float[] Y = new float[] { 0, 0, 1, 1 }; + private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0.5f, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f); + + public static final GlyphEffectMesh INSTANCE = new GlyphEffectMesh(); @Override public int vertexCount() { - return 4 * quads.length; + return 4; } @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 + X[j]); - vertexList.y(quadStart + j, quad.y + Y[j]); - vertexList.z(quadStart + j, quad.z); - vertexList.normalX(quadStart + j, 0); - vertexList.normalY(quadStart + j, 0); - vertexList.normalZ(quadStart + j, 1); - vertexList.overlay(quadStart + j, OverlayTexture.NO_OVERLAY); - vertexList.r(quadStart + j, 1); - vertexList.g(quadStart + j, 1); - vertexList.b(quadStart + j, 1); - vertexList.a(quadStart + j, 1); - } + for (int i = 0; i < 4; i++) { + vertexList.x(i, X[i]); + vertexList.y(i, Y[i]); + vertexList.z(i, 0); + vertexList.r(i, 1); + vertexList.g(i, 1); + vertexList.b(i, 1); + vertexList.a(i, 1); + vertexList.normalX(i, 0); + vertexList.normalY(i, 0); + vertexList.normalZ(i, 1); } } @Override public Vector4fc boundingSphere() { - // FIXME: what is the actual bounding sphere?? - return new Vector4f(0, 0, 0, 2); + return BOUNDING_SPHERE; } } } diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert index 3f7d2a303..6592d5e8c 100644 --- a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert @@ -8,7 +8,7 @@ void flw_instanceVertex(in FlwInstance i) { flw_vertexTexCoord.s = i.u0u1v0v1[(vertexInGlyph & 2u) >> 1u]; flw_vertexTexCoord.t = i.u0u1v0v1[2u + yIndex]; - flw_vertexColor = i.color; + 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; diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java index f3abee51b..c156f5668 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java @@ -12,8 +12,8 @@ import dev.engine_room.flywheel.impl.mixin.ModelPartAccessor; import dev.engine_room.flywheel.impl.mixin.PoseStackAccessor; import dev.engine_room.flywheel.impl.mixin.text.FontAccessor; +import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension; 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; @@ -48,12 +48,17 @@ public Deque getPoseStack(PoseStack stack) { } @Override - public GlyphExtension getGlyphExtension(BakedGlyph glyph) { - return (GlyphExtension) glyph; + public FontSet getFontSet(Font font, ResourceLocation loc) { + return ((FontAccessor) font).flywheel$getFontSet(loc); } @Override - public FontSet getFontSet(Font font, ResourceLocation loc) { - return ((FontAccessor) font).flywheel$getFontSet(loc); + public boolean getFilterFishyGlyphs(Font font) { + return ((FontAccessor) font).flywheel$getFilterFishyGlyphs(); + } + + @Override + public BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph) { + return (BakedGlyphExtension) glyph; } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java b/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java new file mode 100644 index 000000000..2db5e5462 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java @@ -0,0 +1,7 @@ +package dev.engine_room.flywheel.impl.extension; + +import net.minecraft.resources.ResourceLocation; + +public interface FontTextureExtension { + void flywheel$setName(ResourceLocation name); +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java index 2d3c63f24..e18ffb5c5 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java @@ -5,12 +5,12 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; -import dev.engine_room.flywheel.lib.internal.GlyphExtension; +import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension; import net.minecraft.client.gui.font.glyphs.BakedGlyph; import net.minecraft.resources.ResourceLocation; @Mixin(BakedGlyph.class) -public class BakedGlyphMixin implements GlyphExtension { +public class BakedGlyphMixin implements BakedGlyphExtension { @Shadow @Final private float u0; diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java index db9cbe0c4..bb37a7212 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java @@ -1,6 +1,7 @@ package dev.engine_room.flywheel.impl.mixin.text; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; import net.minecraft.client.gui.Font; @@ -9,6 +10,9 @@ @Mixin(Font.class) public interface FontAccessor { + @Accessor("filterFishyGlyphs") + boolean flywheel$getFilterFishyGlyphs(); + @Invoker("getFontSet") FontSet flywheel$getFontSet(ResourceLocation fontLocation); } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java index d31f79a41..8970ae216 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java @@ -8,7 +8,7 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Local; -import dev.engine_room.flywheel.lib.internal.FontTextureExtension; +import dev.engine_room.flywheel.impl.extension.FontTextureExtension; import net.minecraft.client.gui.font.FontSet; import net.minecraft.client.gui.font.FontTexture; import net.minecraft.resources.ResourceLocation; diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java index 358d9e36b..bd00aac1c 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java @@ -21,8 +21,8 @@ import com.mojang.blaze3d.systems.RenderSystem; import dev.engine_room.flywheel.impl.FontTextureUpload; -import dev.engine_room.flywheel.lib.internal.FontTextureExtension; -import dev.engine_room.flywheel.lib.internal.GlyphExtension; +import dev.engine_room.flywheel.impl.extension.FontTextureExtension; +import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension; import net.minecraft.client.gui.font.FontTexture; import net.minecraft.client.gui.font.glyphs.BakedGlyph; import net.minecraft.client.renderer.texture.AbstractTexture; @@ -90,7 +90,7 @@ public abstract class FontTextureMixin extends AbstractTexture implements FontTe @ModifyExpressionValue(method = "add", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/glyphs/BakedGlyph")) private BakedGlyph flywheel$setGlyphExtensionName(BakedGlyph original) { - ((GlyphExtension) original).flywheel$texture(flywheel$name); + ((BakedGlyphExtension) original).flywheel$texture(flywheel$name); return original; } @@ -108,7 +108,7 @@ public abstract class FontTextureMixin extends AbstractTexture implements FontTe } @Override - public void flywheel$setName(ResourceLocation value) { - flywheel$name = value; + public void flywheel$setName(ResourceLocation name) { + flywheel$name = name; } } diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java index 531760ef3..b4dc3e192 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -18,11 +18,13 @@ import dev.engine_room.flywheel.lib.util.ResourceReloadCache; import dev.engine_room.flywheel.lib.visual.AbstractBlockEntityVisual; import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual; -import dev.engine_room.flywheel.lib.visual.text.SimpleTextLayer; import dev.engine_room.flywheel.lib.visual.text.TextLayer; +import dev.engine_room.flywheel.lib.visual.text.TextLayers; import dev.engine_room.flywheel.lib.visual.text.TextVisual; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; import net.minecraft.client.model.geom.ModelLayers; +import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.Sheets; import net.minecraft.util.FastColor; import net.minecraft.util.FormattedCharSequence; @@ -36,8 +38,8 @@ import net.minecraft.world.phys.Vec3; public class SignVisual extends AbstractBlockEntityVisual implements SimpleDynamicVisual { - private static final Vec3 TEXT_OFFSET = new Vec3(0.0, 0.3333333432674408, 0.046666666865348816); + private static final Font FONT = Minecraft.getInstance().font; private static final ResourceReloadCache SIGN_MODELS = new ResourceReloadCache<>(SignVisual::createSignModel); @@ -47,12 +49,12 @@ public class SignVisual extends AbstractBlockEntityVisual imple .backfaceCulling(false) .build(); - private final Matrix4f pose = new Matrix4f(); private final InstanceTree instances; + private final Matrix4f initialPose; // The 8 lines of text we render - private final TextVisual[] frontText = new TextVisual[4]; - private final TextVisual[] backText = new TextVisual[4]; + private final TextVisual[] frontTextVisuals = new TextVisual[4]; + private final TextVisual[] backTextVisuals = new TextVisual[4]; // Need to update these every frame, so just remember which ones are obfuscated // Most of the time this will be empty. @@ -65,8 +67,8 @@ public SignVisual(VisualizationContext ctx, SignBlockEntity blockEntity, float p super(ctx, blockEntity, partialTick); for (int i = 0; i < 4; i++) { - frontText[i] = new TextVisual(ctx.instancerProvider()); - backText[i] = new TextVisual(ctx.instancerProvider()); + frontTextVisuals[i] = new TextVisual(ctx.instancerProvider()); + backTextVisuals[i] = new TextVisual(ctx.instancerProvider()); } var block = (SignBlock) blockState.getBlock(); @@ -79,46 +81,48 @@ public SignVisual(VisualizationContext ctx, SignBlockEntity blockEntity, float p instances.childOrThrow("stick") .visible(isStanding); - var rotation = -block.getYRotationDegrees(blockState); - var visualPosition = getVisualPosition(); - var signModelRenderScale = this.getSignModelRenderScale(); - pose.translate(visualPosition.getX() + 0.5f, visualPosition.getY() + 0.75f * signModelRenderScale, visualPosition.getZ() + 0.5f) + var signModelRenderScale = getSignModelRenderScale(); + var rotation = -block.getYRotationDegrees(blockState); + initialPose = new Matrix4f().translate(visualPosition.getX() + 0.5f, visualPosition.getY() + 0.75f * signModelRenderScale, visualPosition.getZ() + 0.5f) .rotateY(Mth.DEG_TO_RAD * rotation); - if (!(isStanding)) { - pose.translate(0.0f, -0.3125f, -0.4375f); + if (!isStanding) { + initialPose.translate(0.0f, -0.3125f, -0.4375f); } // Only apply this to the instances because text gets a separate scaling. - instances.scale(signModelRenderScale, -signModelRenderScale, -signModelRenderScale); - - instances.updateInstancesStatic(pose); + Matrix4f initialModelPose = new Matrix4f(initialPose).scale(signModelRenderScale, -signModelRenderScale, -signModelRenderScale); + instances.updateInstancesStatic(initialModelPose); lastFrontText = blockEntity.getFrontText(); lastBackText = blockEntity.getBackText(); - this.setupText(lastFrontText, true); - this.setupText(lastBackText, false); + setupText(lastFrontText, true); + setupText(lastBackText, false); + } + + private static ModelTree createSignModel(WoodType woodType) { + return ModelTrees.of(ModelLayers.createSignModelName(woodType), Sheets.getSignMaterial(woodType), MATERIAL); } @Override public void beginFrame(Context ctx) { - boolean setup = false; + boolean doSetup = false; if (lastFrontText != blockEntity.getFrontText()) { lastFrontText = blockEntity.getFrontText(); - setup = true; + doSetup = true; } if (lastBackText != blockEntity.getBackText()) { lastBackText = blockEntity.getBackText(); - setup = true; + doSetup = true; } - if (setup) { + if (doSetup) { // Setup both to make it easier to track obfuscation obfuscated.clear(); - this.setupText(lastBackText, false); - this.setupText(lastFrontText, true); + setupText(lastFrontText, true); + setupText(lastBackText, false); } else { // The is visible check is relatively expensive compared to the boolean checks above, // so only do it when it'll actually save some work in obfuscating. @@ -128,11 +132,6 @@ public void beginFrame(Context ctx) { } } - @Override - public void collectCrumblingInstances(Consumer<@Nullable Instance> consumer) { - instances.traverse(consumer); - } - @Override public void updateLight(float partialTick) { int packedLight = computePackedLight(); @@ -141,120 +140,123 @@ public void updateLight(float partialTick) { .setChanged(); }); - for (var text : frontText) { - text.light(packedLight); + if (!lastFrontText.hasGlowingText()) { + for (var text : frontTextVisuals) { + text.light(packedLight); + text.setup(); + } } - for (var text : backText) { - text.light(packedLight); + + if (!lastBackText.hasGlowingText()) { + for (var text : backTextVisuals) { + text.light(packedLight); + text.setup(); + } } } + @Override + public void collectCrumblingInstances(Consumer<@Nullable Instance> consumer) { + instances.traverse(consumer); + } + @Override protected void _delete() { instances.delete(); - for (var text : frontText) { + for (var text : frontTextVisuals) { text.delete(); } - for (var text : backText) { + for (var text : backTextVisuals) { text.delete(); } } - public float getSignModelRenderScale() { + protected float getSignModelRenderScale() { return 0.6666667f; } - public float getSignTextRenderScale() { + protected float getSignTextRenderScale() { return 0.6666667f; } - public Vec3 getTextOffset() { + protected Vec3 getTextOffset() { return TEXT_OFFSET; } - protected void setupText(SignText text, boolean isFrontText) { - FormattedCharSequence[] formattedCharSequences = text.getRenderMessages(Minecraft.getInstance() + private void setupText(SignText text, boolean isFrontText) { + FormattedCharSequence[] textLines = text.getRenderMessages(Minecraft.getInstance() .isTextFilteringEnabled(), component -> { - List list = Minecraft.getInstance().font.split(component, blockEntity.getMaxTextLineWidth()); + List list = FONT.split(component, blockEntity.getMaxTextLineWidth()); return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); }); List layers = new ArrayList<>(); - int darkColor = TextLayer.GlyphColor.adjustColor(getDarkColor(text)); + int darkColor = getDarkColor(text); int textColor; if (text.hasGlowingText()) { - textColor = TextLayer.GlyphColor.adjustColor(text.getColor() - .getTextColor()); + textColor = text.getColor() + .getTextColor(); - layers.add(new SimpleTextLayer.Builder().style(TextLayer.GlyphMeshStyle.OUTLINE) - .material(TextLayer.GlyphMaterial.SIMPLE) - .color(TextLayer.GlyphColor.always(darkColor)) - .build()); + layers.add(TextLayers.outline(darkColor)); } else { textColor = darkColor; } - layers.add(new SimpleTextLayer.Builder().style(TextLayer.GlyphMeshStyle.SIMPLE) - .material(TextLayer.GlyphMaterial.POLYGON_OFFSET) - .color(TextLayer.GlyphColor.defaultTo(textColor)) - .bias(1) - .build()); + layers.add(TextLayers.normal(textColor, Font.DisplayMode.POLYGON_OFFSET, 1)); - var dst = isFrontText ? frontText : backText; + var textVisuals = isFrontText ? frontTextVisuals : backTextVisuals; int lineHeight = blockEntity.getTextLineHeight(); int lineDelta = 4 * lineHeight / 2; - for (int m = 0; m < 4; ++m) { - FormattedCharSequence formattedCharSequence = formattedCharSequences[m]; - float f = (float) -Minecraft.getInstance().font.width(formattedCharSequence) / 2; - - var textVisual = dst[m].content(formattedCharSequence) - .layers(layers) - .fullBright(text.hasGlowingText()) - .backgroundColor(0) - .x(f) - .y(m * lineHeight - lineDelta); + for (int i = 0; i < 4; ++i) { + FormattedCharSequence textLine = textLines[i]; + float x = (float) (-FONT.width(textLine) / 2); + float y = i * lineHeight - lineDelta; - var textPose = textVisual.pose(); - - textPose.set(pose); + var textVisual = textVisuals[i].layers(layers) + .text(textLine) + .pos(x, y) + .backgroundColor(0); + var pose = textVisual.pose().set(initialPose); if (!isFrontText) { - textPose.rotateY(Mth.PI); + pose.rotateY(Mth.PI); + } + float scale = 0.015625f * getSignTextRenderScale(); + var textOffset = getTextOffset(); + pose.translate((float) textOffset.x, (float) textOffset.y, (float) textOffset.z); + pose.scale(scale, -scale, scale); + + if (text.hasGlowingText()) { + textVisual.light(LightTexture.FULL_BRIGHT); } - var offset = getTextOffset(); - float scale = 0.015625f * this.getSignTextRenderScale(); - textPose.translate((float) offset.x, (float) offset.y, (float) offset.z); - textPose.scale(scale, -scale, scale); + // FIXME: incorrect light when going from glowing to non-glowing textVisual.setup(); - if (hasObfuscation(formattedCharSequence)) { - this.obfuscated.add(textVisual); + if (hasObfuscation(textLine)) { + obfuscated.add(textVisual); } } } - public static boolean hasObfuscation(FormattedCharSequence text) { - return text.accept((i, s, j) -> s.isObfuscated()); - } - - public static int getDarkColor(SignText signText) { - int i = signText.getColor() + private static int getDarkColor(SignText signText) { + int colorArgb = signText.getColor() .getTextColor(); - if (i == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) { - return -988212; + if (colorArgb == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) { + return 0xFFF0EBCC; } - int j = (int) ((double) FastColor.ARGB32.red(i) * 0.4); - int k = (int) ((double) FastColor.ARGB32.green(i) * 0.4); - int l = (int) ((double) FastColor.ARGB32.blue(i) * 0.4); - return FastColor.ARGB32.color(0, j, k, l); + + int r = (int) ((double) FastColor.ARGB32.red(colorArgb) * 0.4); + int g = (int) ((double) FastColor.ARGB32.green(colorArgb) * 0.4); + int b = (int) ((double) FastColor.ARGB32.blue(colorArgb) * 0.4); + return FastColor.ARGB32.color(0, r, g, b); } - private static ModelTree createSignModel(WoodType woodType) { - return ModelTrees.of(ModelLayers.createSignModelName(woodType), Sheets.getSignMaterial(woodType), MATERIAL); + private static boolean hasObfuscation(FormattedCharSequence text) { + return text.accept((i, s, j) -> s.isObfuscated()); } } diff --git a/gradle.properties b/gradle.properties index 013c0181a..418aaff91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ parchment_version = 2023.09.03 # Minecraft build dependency versions minecraft_version = 1.20.1 forge_version = 47.2.19 -fabric_loader_version=0.16.5 +fabric_loader_version = 0.16.5 fabric_api_version = 0.92.1+1.20.1 # Build dependency mod versions From 39b2508d0259d3c18fb872c43b3e9f15c6aa1c4b Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Sat, 5 Oct 2024 21:21:21 -0700 Subject: [PATCH 11/14] The sad truth, baited - Fix packed uvs from being saturated --- .../engine_room/flywheel/lib/instance/GlyphInstance.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java index c5e999d9f..4278913a7 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -60,7 +60,9 @@ private void setUvs(BakedGlyphExtension glyphExtension) { float v0 = glyphExtension.flywheel$v0(); float v1 = glyphExtension.flywheel$v1(); - packedUs = ((int) DataPacker.packNormU16(u1) << 16) | (int) DataPacker.packNormU16(u0); - packedVs = ((int) DataPacker.packNormU16(v1) << 16) | (int) DataPacker.packNormU16(v0); + // Need to make sure at least u0/v0 don't get their sign bit extended in the cast. + // It causes u1/v1 to be completely saturated. + packedUs = (Short.toUnsignedInt(DataPacker.packNormU16(u1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(u0)); + packedVs = (Short.toUnsignedInt(DataPacker.packNormU16(v1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(v0)); } } From 47b6648681861056f49bf6557acc5a9b29454cc8 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Sat, 5 Oct 2024 21:54:18 -0700 Subject: [PATCH 12/14] 20/20 vision - Calculate actual bounding sphere for glyph meshes - Cull objects that are less than a pixel on the screen --- .../flywheel/internal/indirect/cull.glsl | 54 +++++++++++-------- .../flywheel/lib/visual/text/TextVisual.java | 33 +++++++++++- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl index 395979d5c..2817e4b69 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl @@ -87,38 +87,46 @@ bool _flw_isVisible(uint instanceIndex, uint modelIndex) { transformBoundingSphere(flw_view, center, radius); vec4 aabb; - if (projectSphere(center, radius, _flw_cullData.znear, _flw_cullData.P00, _flw_cullData.P11, aabb)) - { - float width = (aabb.z - aabb.x) * _flw_cullData.pyramidWidth; - float height = (aabb.w - aabb.y) * _flw_cullData.pyramidHeight; + if (projectSphere(center, radius, _flw_cullData.znear, _flw_cullData.P00, _flw_cullData.P11, aabb)) { + vec2 size = aabb.zw - aabb.xy; - int level = clamp(int(ceil(log2(max(width, height)))), 0, _flw_cullData.pyramidLevels); + vec2 sizeInPixels = size * flw_viewportSize; - ivec2 levelSize = textureSize(_flw_depthPyramid, level); + // Cull objects that are less than a pixel in size. Could probably make this configurable. + isVisible = isVisible && any(greaterThan(sizeInPixels, vec2(1.))); - ivec4 levelSizePair = ivec4(levelSize, levelSize); + if (isVisible) { + float width = size.x * _flw_cullData.pyramidWidth; + float height = size.y * _flw_cullData.pyramidHeight; - ivec4 bounds = ivec4(aabb * vec4(levelSizePair)); + int level = clamp(int(ceil(log2(max(width, height)))), 0, _flw_cullData.pyramidLevels); - // Clamp to the texture bounds. - // Since we're not going through a sampler out of bounds texel fetches will return 0. - bounds = clamp(bounds, ivec4(0), levelSizePair); + ivec2 levelSize = textureSize(_flw_depthPyramid, level); - float depth01 = texelFetch(_flw_depthPyramid, bounds.xw, level).r; - float depth11 = texelFetch(_flw_depthPyramid, bounds.zw, level).r; - float depth10 = texelFetch(_flw_depthPyramid, bounds.zy, level).r; - float depth00 = texelFetch(_flw_depthPyramid, bounds.xy, level).r; + ivec4 levelSizePair = ivec4(levelSize, levelSize); - float depth; - if (_flw_cullData.useMin == 0) { - depth = max(max(depth00, depth01), max(depth10, depth11)); - } else { - depth = min(min(depth00, depth01), min(depth10, depth11)); - } + ivec4 bounds = ivec4(aabb * vec4(levelSizePair)); + + // Clamp to the texture bounds. + // Since we're not going through a sampler out of bounds texel fetches will return 0. + bounds = clamp(bounds, ivec4(0), levelSizePair); + + float depth01 = texelFetch(_flw_depthPyramid, bounds.xw, level).r; + float depth11 = texelFetch(_flw_depthPyramid, bounds.zw, level).r; + float depth10 = texelFetch(_flw_depthPyramid, bounds.zy, level).r; + float depth00 = texelFetch(_flw_depthPyramid, bounds.xy, level).r; - float depthSphere = 1. + _flw_cullData.znear / (center.z + radius); + float depth; + if (_flw_cullData.useMin == 0) { + depth = max(max(depth00, depth01), max(depth10, depth11)); + } else { + depth = min(min(depth00, depth01), min(depth10, depth11)); + } - isVisible = isVisible && depthSphere <= depth; + float depthSphere = 1. + _flw_cullData.znear / (center.z + radius); + + isVisible = isVisible && depthSphere <= depth; + } } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java index 677a60ab2..14b72b742 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -308,9 +308,8 @@ private record GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offset private static final float[] X = new float[] { 0, 0, 1, 1 }; private static final float[] Y = new float[] { 0, 1, 1, 0 }; - // FIXME: what is the actual bounding sphere?? public GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets) { - this(glyphWidth, glyphHeight, offsets, new Vector4f(0, 0, 0, Math.max(glyphWidth, glyphHeight) * 2 * Mth.SQRT_OF_TWO)); + this(glyphWidth, glyphHeight, offsets, boundingSphere(glyphWidth, glyphHeight, offsets)); } @Override @@ -344,6 +343,36 @@ public void write(MutableVertexList vertexList) { public Vector4fc boundingSphere() { return boundingSphere; } + + private static Vector4fc boundingSphere(float glyphWidth, float glyphHeight, Vector2fc[] offsets) { + if (offsets.length == 0) { + return new Vector4f(0, 0, 0, 0); + } + + float minX = Float.POSITIVE_INFINITY; + float minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY; + float maxY = Float.NEGATIVE_INFINITY; + for (Vector2fc offset : offsets) { + for (int j = 0; j < 4; j++) { + var x = offset.x() + (glyphWidth * X[j]); + var y = offset.y() + (glyphHeight * Y[j]); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + + float x = (minX + maxX) / 2; + float y = (minY + maxY) / 2; + + float sizeX = maxX - minX; + float sizeY = maxY - minY; + float maxSize = Math.max(sizeX, sizeY); + + return new Vector4f(x, y, 0, Mth.SQRT_OF_TWO * maxSize / 2); + } } private record GlyphEffectMesh() implements QuadMesh { From c3ac594d372053fe6c56427b98222c9356907335 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Wed, 9 Oct 2024 21:29:40 -0700 Subject: [PATCH 13/14] Dante's printing press - Move text layer handling to the inner loop of Sink - Make key methods static and pass layer as a parameter --- .../flywheel/lib/visual/text/TextVisual.java | 103 +++++++++--------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java index 14b72b742..e11b6683a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -138,16 +138,11 @@ public void setup() { recycler.resetCount(); var sink = SINKS.get(); - sink.prepare(recycler, pose, light); + sink.prepare(recycler, layers, pose, light, x, y); - int maxX = 0; - for (TextLayer layer : layers) { - sink.prepareForLayer(layer, x, y); - text.accept(sink); - maxX = Math.max(maxX, (int) sink.x); - } + text.accept(sink); - sink.addBackground(backgroundColor, x, maxX); + sink.addBackground(backgroundColor, x, sink.x); sink.clear(); recycler.discardExtra(); @@ -195,30 +190,27 @@ private static class Sink implements FormattedCharSink { @UnknownNullability private SmartRecycler recycler; @UnknownNullability + private List layers; + @UnknownNullability private Matrix4f pose; private int light; - @UnknownNullability - private TextLayer layer; private float x; private float y; - public void prepare(SmartRecycler recycler, Matrix4f pose, int light) { + public void prepare(SmartRecycler recycler, List layers, Matrix4f pose, int light, float x, float y) { this.recycler = recycler; + this.layers = layers; this.pose = pose; this.light = light; - } - - public void prepareForLayer(TextLayer layer, float x, float y) { - this.layer = layer; this.x = x; this.y = y; } public void clear() { recycler = null; + layers = null; pose = null; - layer = null; } @Override @@ -228,28 +220,32 @@ public boolean accept(int index, Style style, int codePoint) { BakedGlyph glyph = style.isObfuscated() && codePoint != ' ' ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(codePoint); boolean bold = style.isBold(); - int color = layer.color() - .color(style.getColor()); - Vector2fc offset = layer.offset(); - - if (!(glyph instanceof EmptyGlyph)) { - GlyphInstance instance = recycler.get(key(glyphInfo, glyph, layer.pattern(), bold)); - float shadowOffset = glyphInfo.getShadowOffset(); - instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, y + offset.y() * shadowOffset, style.isItalic()); - instance.colorArgb(color); - instance.light(light); - instance.setChanged(); - } - float advance = glyphInfo.getAdvance(bold); - // SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer. - float effectOffsetX = offset.x(); - float effectOffsetY = offset.y(); - if (style.isStrikethrough()) { - addEffect(x + effectOffsetX - 1.0f, y + effectOffsetY + 4.5f, x + effectOffsetX + advance, y + effectOffsetY + 4.5f - 1.0f, 0.01f, color); - } - if (style.isUnderlined()) { - addEffect(x + effectOffsetX - 1.0f, y + effectOffsetY + 9.0f, x + effectOffsetX + advance, y + effectOffsetY + 9.0f - 1.0f, 0.01f, color); + + // Process layers in the inner loop for 2 reasons: + // 1. So we don't have to iterate over all the text and to the same glyph lookups for each layer + // 2. So we get the same random draw in each layer for obfuscated text + for (TextLayer layer : layers) { + int color = layer.color() + .color(style.getColor()); + Vector2fc offset = layer.offset(); + + if (!(glyph instanceof EmptyGlyph)) { + GlyphInstance instance = recycler.get(key(layer, glyphInfo, glyph, bold)); + float shadowOffset = glyphInfo.getShadowOffset(); + instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, y + offset.y() * shadowOffset, style.isItalic()); + instance.colorArgb(color); + instance.light(light); + instance.setChanged(); + } + + // SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer. + if (style.isStrikethrough()) { + addEffect(layer, x + offset.x() - 1.0f, y + offset.y() + 4.5f, x + offset.x() + advance, y + offset.y() + 4.5f - 1.0f, 0.01f, color); + } + if (style.isUnderlined()) { + addEffect(layer, x + offset.x() - 1.0f, y + offset.y() + 9.0f, x + offset.x() + advance, y + offset.y() + 9.0f - 1.0f, 0.01f, color); + } } x += advance; @@ -258,43 +254,52 @@ public boolean accept(int index, Style style, int codePoint) { public void addBackground(int backgroundColor, float startX, float endX) { if (backgroundColor != 0) { - addEffect(startX - 1.0f, y + 9.0f, endX + 1.0f, y - 1.0f, 0.01f, backgroundColor); + BakedGlyph glyph = FlwLibLink.INSTANCE.getFontSet(FONT, Style.DEFAULT_FONT) + .whiteGlyph(); + + var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); + + GlyphInstance instance = recycler.get(effectKey(glyphExtension.flywheel$texture(), TextLayer.GlyphMaterial.SEE_THROUGH, 0)); + instance.setEffect(glyph, pose, startX - 1.0f, y + 9.0f, endX + 1.0f, y - 1.0f, 0.01f); + instance.colorArgb(backgroundColor); + instance.light(light); + instance.setChanged(); } } - private void addEffect(float x0, float y0, float x1, float y1, float depth, int colorArgb) { + private void addEffect(TextLayer layer, float x0, float y0, float x1, float y1, float depth, int colorArgb) { BakedGlyph glyph = FlwLibLink.INSTANCE.getFontSet(FONT, Style.DEFAULT_FONT) .whiteGlyph(); - GlyphInstance instance = recycler.get(effectKey(glyph)); + GlyphInstance instance = recycler.get(effectKey(glyph, layer)); instance.setEffect(glyph, pose, x0, y0, x1, y1, depth); instance.colorArgb(colorArgb); instance.light(light); instance.setChanged(); } - private GlyphInstanceKey key(GlyphInfo glyphInfo, BakedGlyph glyph, TextLayer.GlyphPattern pattern, boolean bold) { + private static GlyphInstanceKey key(TextLayer layer, GlyphInfo glyphInfo, BakedGlyph glyph, boolean bold) { var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); float glyphWidth = glyphExtension.flywheel$right() - glyphExtension.flywheel$left(); float glyphHeight = glyphExtension.flywheel$down() - glyphExtension.flywheel$up(); - return key(glyphWidth, glyphHeight, glyphExtension.flywheel$texture(), pattern, bold, bold ? glyphInfo.getBoldOffset() : 0, glyphInfo.getShadowOffset()); + return key(layer, glyphWidth, glyphHeight, glyphExtension.flywheel$texture(), bold, bold ? glyphInfo.getBoldOffset() : 0, glyphInfo.getShadowOffset()); } - private GlyphInstanceKey key(float glyphWidth, float glyphHeight, ResourceLocation texture, TextLayer.GlyphPattern pattern, boolean bold, float boldOffset, float shadowOffset) { - var meshKey = new GlyphMeshKey(glyphWidth, glyphHeight, pattern, bold, boldOffset, shadowOffset); + private static GlyphInstanceKey key(TextLayer layer, float glyphWidth, float glyphHeight, ResourceLocation texture, boolean bold, float boldOffset, float shadowOffset) { + var meshKey = new GlyphMeshKey(glyphWidth, glyphHeight, layer.pattern(), bold, boldOffset, shadowOffset); var modelKey = new GlyphModelKey(meshKey, layer.material(), texture); return new GlyphInstanceKey(modelKey, layer.bias()); } - private GlyphInstanceKey effectKey(BakedGlyph glyph) { + private static GlyphInstanceKey effectKey(BakedGlyph glyph, TextLayer layer) { var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); - return effectKey(glyphExtension.flywheel$texture()); + return effectKey(glyphExtension.flywheel$texture(), layer.material(), layer.bias()); } - private GlyphInstanceKey effectKey(ResourceLocation texture) { - var modelKey = new GlyphModelKey(null, layer.material(), texture); - return new GlyphInstanceKey(modelKey, layer.bias()); + private static GlyphInstanceKey effectKey(ResourceLocation texture, TextLayer.GlyphMaterial material, int bias) { + var modelKey = new GlyphModelKey(null, material, texture); + return new GlyphInstanceKey(modelKey, bias); } } From 1091a478e15a502002811e85f2ffd1a4452fe200 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Wed, 9 Oct 2024 21:59:52 -0700 Subject: [PATCH 14/14] Pair down parameters per Pepper's pondering - Flatten TextVisual API to be less of a parameter container - This is mostly a surface change - Do not accept x and y positions, instead force the user to translate in their matrix - Track light from light updates to apply when setting up text - Add (unoptimized) updateLight, backgroundColor, and updateObfuscated methods to TextVisual --- .../flywheel/lib/visual/text/TextVisual.java | 104 ++++++------------ .../flywheel/vanilla/SignVisual.java | 28 ++--- 2 files changed, 45 insertions(+), 87 deletions(-) diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java index e11b6683a..cac40b934 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -1,7 +1,6 @@ package dev.engine_room.flywheel.lib.visual.text; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import org.jetbrains.annotations.Nullable; @@ -53,8 +52,6 @@ public final class TextVisual { private final Matrix4f pose = new Matrix4f(); private FormattedCharSequence text = FormattedCharSequence.EMPTY; - private float x; - private float y; private int backgroundColor = 0; private int light; @@ -63,91 +60,60 @@ public TextVisual(InstancerProvider provider) { .createInstance()); } - public TextVisual addLayer(TextLayer layer) { - layers.add(layer); - return this; - } - - public TextVisual addLayers(Collection layers) { - this.layers.addAll(layers); - return this; - } - - public TextVisual layers(Collection layers) { + public void setup(FormattedCharSequence textLine, List layers, Matrix4f pose, int light) { + // TODO: probably don't store everything + this.text = textLine; this.layers.clear(); this.layers.addAll(layers); - return this; - } + this.pose.set(pose); + this.light = light; - public TextVisual clearLayers() { - layers.clear(); - return this; + setup(); } - public Matrix4f pose() { - return pose; + public void updateObfuscated() { + // TODO: track obfuscated glyphs and update here + setup(); } - public TextVisual text(FormattedCharSequence text) { - this.text = text; - return this; + public void backgroundColor(int backgroundColor) { + // TODO: don't setup the whole thing + this.backgroundColor = backgroundColor; + setup(); } - public TextVisual x(float x) { - this.x = x; - return this; + public void updateLight(int packedLight) { + // TODO: just iterate over instances and update light + light = packedLight; + setup(); } - public TextVisual y(float y) { - this.y = y; - return this; - } + private void setup() { + recycler.resetCount(); - public TextVisual pos(float x, float y) { - this.x = x; - this.y = y; - return this; - } + var sink = SINKS.get(); + sink.prepare(recycler, layers, pose, light); - public TextVisual backgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - return this; - } + text.accept(sink); - public TextVisual light(int light) { - this.light = light; - return this; + sink.addBackground(backgroundColor, 0, sink.x); + sink.clear(); + + recycler.discardExtra(); } - public TextVisual reset() { + private TextVisual reset() { + // TODO: should this be public? what should it do? layers.clear(); pose.identity(); text = FormattedCharSequence.EMPTY; - x = 0; - y = 0; backgroundColor = 0; light = 0; return this; } - // TODO: track glyph instances and add method to update only UVs of obfuscated glyphs, method to update only - // background color, and method to only update light - public void setup() { - recycler.resetCount(); - - var sink = SINKS.get(); - sink.prepare(recycler, layers, pose, light, x, y); - - text.accept(sink); - - sink.addBackground(backgroundColor, x, sink.x); - sink.clear(); - - recycler.discardExtra(); - } - public void delete() { recycler.delete(); } @@ -196,15 +162,13 @@ private static class Sink implements FormattedCharSink { private int light; private float x; - private float y; - public void prepare(SmartRecycler recycler, List layers, Matrix4f pose, int light, float x, float y) { + public void prepare(SmartRecycler recycler, List layers, Matrix4f pose, int light) { this.recycler = recycler; this.layers = layers; this.pose = pose; this.light = light; - this.x = x; - this.y = y; + this.x = 0; } public void clear() { @@ -233,7 +197,7 @@ public boolean accept(int index, Style style, int codePoint) { if (!(glyph instanceof EmptyGlyph)) { GlyphInstance instance = recycler.get(key(layer, glyphInfo, glyph, bold)); float shadowOffset = glyphInfo.getShadowOffset(); - instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, y + offset.y() * shadowOffset, style.isItalic()); + instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, offset.y() * shadowOffset, style.isItalic()); instance.colorArgb(color); instance.light(light); instance.setChanged(); @@ -241,10 +205,10 @@ public boolean accept(int index, Style style, int codePoint) { // SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer. if (style.isStrikethrough()) { - addEffect(layer, x + offset.x() - 1.0f, y + offset.y() + 4.5f, x + offset.x() + advance, y + offset.y() + 4.5f - 1.0f, 0.01f, color); + addEffect(layer, x + offset.x() - 1.0f, offset.y() + 4.5f, x + offset.x() + advance, offset.y() + 4.5f - 1.0f, 0.01f, color); } if (style.isUnderlined()) { - addEffect(layer, x + offset.x() - 1.0f, y + offset.y() + 9.0f, x + offset.x() + advance, y + offset.y() + 9.0f - 1.0f, 0.01f, color); + addEffect(layer, x + offset.x() - 1.0f, offset.y() + 9.0f, x + offset.x() + advance, offset.y() + 9.0f - 1.0f, 0.01f, color); } } @@ -260,7 +224,7 @@ public void addBackground(int backgroundColor, float startX, float endX) { var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph); GlyphInstance instance = recycler.get(effectKey(glyphExtension.flywheel$texture(), TextLayer.GlyphMaterial.SEE_THROUGH, 0)); - instance.setEffect(glyph, pose, startX - 1.0f, y + 9.0f, endX + 1.0f, y - 1.0f, 0.01f); + instance.setEffect(glyph, pose, startX - 1.0f, 9.0f, endX + 1.0f, 1.0f, 0.01f); instance.colorArgb(backgroundColor); instance.light(light); instance.setChanged(); diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java index b4dc3e192..c772bb425 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -60,6 +60,8 @@ public class SignVisual extends AbstractBlockEntityVisual imple // Most of the time this will be empty. private final List obfuscated = new ArrayList<>(); + private int packedLight = 0; + private SignText lastFrontText; private SignText lastBackText; @@ -127,14 +129,14 @@ public void beginFrame(Context ctx) { // The is visible check is relatively expensive compared to the boolean checks above, // so only do it when it'll actually save some work in obfuscating. if (isVisible(ctx.frustum())) { - obfuscated.forEach(TextVisual::setup); + obfuscated.forEach(TextVisual::updateObfuscated); } } } @Override public void updateLight(float partialTick) { - int packedLight = computePackedLight(); + packedLight = computePackedLight(); instances.traverse(instance -> { instance.light(packedLight) .setChanged(); @@ -142,15 +144,13 @@ public void updateLight(float partialTick) { if (!lastFrontText.hasGlowingText()) { for (var text : frontTextVisuals) { - text.light(packedLight); - text.setup(); + text.updateLight(packedLight); } } if (!lastBackText.hasGlowingText()) { for (var text : backTextVisuals) { - text.light(packedLight); - text.setup(); + text.updateLight(packedLight); } } } @@ -216,12 +216,7 @@ private void setupText(SignText text, boolean isFrontText) { float x = (float) (-FONT.width(textLine) / 2); float y = i * lineHeight - lineDelta; - var textVisual = textVisuals[i].layers(layers) - .text(textLine) - .pos(x, y) - .backgroundColor(0); - - var pose = textVisual.pose().set(initialPose); + var pose = new Matrix4f(initialPose); if (!isFrontText) { pose.rotateY(Mth.PI); } @@ -229,13 +224,12 @@ private void setupText(SignText text, boolean isFrontText) { var textOffset = getTextOffset(); pose.translate((float) textOffset.x, (float) textOffset.y, (float) textOffset.z); pose.scale(scale, -scale, scale); + pose.translate(x, y, 0.0f); - if (text.hasGlowingText()) { - textVisual.light(LightTexture.FULL_BRIGHT); - } - // FIXME: incorrect light when going from glowing to non-glowing - textVisual.setup(); + var textVisual = textVisuals[i]; + int light = text.hasGlowingText() ? LightTexture.FULL_BRIGHT : packedLight; + textVisual.setup(textLine, layers, pose, light); if (hasObfuscation(textLine)) { obfuscated.add(textVisual);