From c0b19a2f01f78de93171915dd36ca1cc068bf728 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Fri, 27 Sep 2024 14:20:02 -0700 Subject: [PATCH] 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.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 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 class SignVisual extends AbstractBlockEntityVisual imple .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 class SignVisual extends AbstractBlockEntityVisual imple } 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]; - 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 textVisual = dst[m].content(formattedCharSequence) + .layers(layers) + .fullBright(text.hasGlowingText()) + .backgroundColor(0) + .x(f) + .y(m * lineHeight - lineDelta); - textVisual.pose.set(pose); + var textPose = textVisual.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(); }