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:
Jozufozu 2024-09-27 14:20:02 -07:00
parent 7aefbee9c1
commit c0b19a2f01
5 changed files with 593 additions and 327 deletions

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}