mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2025-01-30 23:04:57 +01:00
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
This commit is contained in:
parent
7aefbee9c1
commit
c0b19a2f01
5 changed files with 593 additions and 327 deletions
|
@ -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<GlyphModelKey, GlyphInstance> 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<GlyphModelKey, Model> GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into);
|
||||
private static final ResourceReloadCache<GlyphSettings, GlyphMesh> 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<Vector3f> 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<Vector3f> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Vector3f> out);
|
||||
|
||||
}
|
||||
}
|
|
@ -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<Sink> SINKS = ThreadLocal.withInitial(Sink::new);
|
||||
|
||||
private final SmartRecycler<GlyphInstancerKey, GlyphInstance> 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<TextLayer> 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<TextLayer> 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<GlyphInstancerKey, GlyphInstance> 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<GlyphInstancerKey, GlyphInstance> 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<GlyphModelKey, Model> GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into);
|
||||
private static final ResourceReloadCache<GlyphMeshKey, GlyphMesh> 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<Vector3f> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<SignBlockEntity> 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<SignBlockEntity> imple
|
|||
}
|
||||
|
||||
void setupText(SignText text, TextVisual[] dst, boolean isFrontText) {
|
||||
var font = Minecraft.getInstance().font;
|
||||
FormattedCharSequence[] formattedCharSequences = text.getRenderMessages(Minecraft.getInstance()
|
||||
.isTextFilteringEnabled(), component -> {
|
||||
List<FormattedCharSequence> list = font.split(component, blockEntity.getMaxTextLineWidth());
|
||||
List<FormattedCharSequence> list = Minecraft.getInstance().font.split(component, blockEntity.getMaxTextLineWidth());
|
||||
return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0);
|
||||
});
|
||||
|
||||
int light;
|
||||
boolean outline;
|
||||
List<TextLayer> 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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue