mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2025-02-07 10:45:00 +01:00
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
This commit is contained in:
parent
9227631c73
commit
71cf582e97
6 changed files with 513 additions and 213 deletions
|
@ -92,6 +92,7 @@ public class EngineImpl implements Engine {
|
||||||
@Override
|
@Override
|
||||||
public void setupRender(RenderContext context) {
|
public void setupRender(RenderContext context) {
|
||||||
try (var state = GlStateTracker.getRestoreState()) {
|
try (var state = GlStateTracker.getRestoreState()) {
|
||||||
|
// Process the render queue for font updates
|
||||||
RenderSystem.replayQueue();
|
RenderSystem.replayQueue();
|
||||||
Uniforms.update(context);
|
Uniforms.update(context);
|
||||||
environmentStorage.flush();
|
environmentStorage.flush();
|
||||||
|
|
|
@ -83,6 +83,10 @@ public class GlyphInstance extends AbstractInstance {
|
||||||
return color((byte) red, (byte) green, (byte) 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) {
|
public GlyphInstance color(byte red, byte green, byte blue, byte alpha) {
|
||||||
this.red = red;
|
this.red = red;
|
||||||
this.green = green;
|
this.green = green;
|
||||||
|
|
|
@ -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<GlyphModelKey, GlyphInstance> recycler;
|
|
||||||
|
|
||||||
Font font;
|
|
||||||
private boolean dropShadow;
|
|
||||||
private float dimFactor;
|
|
||||||
private byte r;
|
|
||||||
private byte g;
|
|
||||||
private byte b;
|
|
||||||
private byte a;
|
|
||||||
private Font.DisplayMode mode;
|
|
||||||
private int packedLightCoords;
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
|
|
||||||
private InstanceEmitter(InstancerProvider instancerProvider) {
|
|
||||||
recycler = new SmartRecycler<>(key -> instancerProvider.instancer(InstanceTypes.GLYPH, GLYPH_CACHE.get(key))
|
|
||||||
.createInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private List<BakedGlyph.Effect> effects;
|
|
||||||
|
|
||||||
private void addEffect(BakedGlyph.Effect effect) {
|
|
||||||
if (this.effects == null) {
|
|
||||||
this.effects = Lists.newArrayList();
|
|
||||||
}
|
|
||||||
this.effects.add(effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean accept(int i, Style style, int j) {
|
|
||||||
float n;
|
|
||||||
float l;
|
|
||||||
float h;
|
|
||||||
float g;
|
|
||||||
FontSet fontSet = FlwLibLink.INSTANCE.getFontSet(font, style.getFont());
|
|
||||||
GlyphInfo glyphInfo = fontSet.getGlyphInfo(j, false);
|
|
||||||
BakedGlyph bakedGlyph = style.isObfuscated() && j != 32 ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(j);
|
|
||||||
boolean bl = style.isBold();
|
|
||||||
float f = this.a;
|
|
||||||
TextColor textColor = style.getColor();
|
|
||||||
if (textColor != null) {
|
|
||||||
int k = textColor.getValue();
|
|
||||||
g = (float) (k >> 16 & 0xFF) / 255.0f * this.dimFactor;
|
|
||||||
h = (float) (k >> 8 & 0xFF) / 255.0f * this.dimFactor;
|
|
||||||
l = (float) (k & 0xFF) / 255.0f * this.dimFactor;
|
|
||||||
} else {
|
|
||||||
g = this.r;
|
|
||||||
h = this.g;
|
|
||||||
l = this.b;
|
|
||||||
}
|
|
||||||
if (!(bakedGlyph instanceof EmptyGlyph)) {
|
|
||||||
// var renderType = bakedGlyph.renderType(Font.DisplayMode.NORMAL);
|
|
||||||
|
|
||||||
var glyphExtension = FlwLibLink.INSTANCE.getGlyphExtension(bakedGlyph);
|
|
||||||
|
|
||||||
GlyphInstance glyph = recycler.get(new GlyphModelKey(glyphExtension.flywheel$texture(), bl, dropShadow));
|
|
||||||
|
|
||||||
glyph.setGlyph(bakedGlyph, this.x, this.y, style.isItalic());
|
|
||||||
glyph.pose.set(pose);
|
|
||||||
glyph.light = LightTexture.FULL_BRIGHT;
|
|
||||||
glyph.setChanged();
|
|
||||||
}
|
|
||||||
float m = glyphInfo.getAdvance(bl);
|
|
||||||
float f2 = n = this.dropShadow ? 1.0f : 0.0f;
|
|
||||||
// if (style.isStrikethrough()) {
|
|
||||||
// this.addEffect(new BakedGlyph.Effect(this.x + n - 1.0f, this.y + n + 4.5f, this.x + n + m, this.y + n + 4.5f - 1.0f, 0.01f, g, h, l, f));
|
|
||||||
// }
|
|
||||||
// if (style.isUnderlined()) {
|
|
||||||
// this.addEffect(new BakedGlyph.Effect(this.x + n - 1.0f, this.y + n + 9.0f, this.x + n + m, this.y + n + 9.0f - 1.0f, 0.01f, g, h, l, f));
|
|
||||||
// }
|
|
||||||
this.x += m;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float finish(int backgroundColor, float x) {
|
|
||||||
if (backgroundColor != 0) {
|
|
||||||
float f = (float) (backgroundColor >> 24 & 0xFF) / 255.0f;
|
|
||||||
float g = (float) (backgroundColor >> 16 & 0xFF) / 255.0f;
|
|
||||||
float h = (float) (backgroundColor >> 8 & 0xFF) / 255.0f;
|
|
||||||
float i = (float) (backgroundColor & 0xFF) / 255.0f;
|
|
||||||
this.addEffect(new BakedGlyph.Effect(x - 1.0f, this.y + 9.0f, this.x + 1.0f, this.y - 1.0f, 0.01f, g, h, i, f));
|
|
||||||
}
|
|
||||||
// if (this.effects != null) {
|
|
||||||
// BakedGlyph bakedGlyph = font.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
|
|
||||||
// VertexConsumer vertexConsumer = this.bufferSource.getBuffer(bakedGlyph.renderType(this.mode));
|
|
||||||
// for (BakedGlyph.Effect effect : this.effects) {
|
|
||||||
// bakedGlyph.renderEffect(effect, this.pose, vertexConsumer, this.packedLightCoords);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return this.x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final ResourceReloadCache<GlyphModelKey, Model> GLYPH_CACHE = new ResourceReloadCache<>(GlyphModelKey::into);
|
|
||||||
|
|
||||||
private static final Material GLYPH_MATERIAL = SimpleMaterial.builder()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private record GlyphModelKey(ResourceLocation font, boolean bold, boolean shadow) {
|
|
||||||
private Model into() {
|
|
||||||
// bold -> x + 1
|
|
||||||
// shadow -> x + 1, y + 1
|
|
||||||
return new SingleMeshModel(new GlyphMesh(new Vector3f[]{new Vector3f(0, 0, 0),}), SimpleMaterial.builderOf(GLYPH_MATERIAL)
|
|
||||||
.texture(font)
|
|
||||||
.cutout(CutoutShaders.ONE_TENTH)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GlyphMesh(Vector3f[] quads) implements QuadMesh {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int vertexCount() {
|
|
||||||
return 4 * quads.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(MutableVertexList vertexList) {
|
|
||||||
for (int i = 0; i < quads.length; i++) {
|
|
||||||
Vector3f quad = quads[i];
|
|
||||||
var quadStart = i * 4;
|
|
||||||
|
|
||||||
for (int j = 0; j < 4; j++) {
|
|
||||||
vertexList.x(quadStart + j, quad.x);
|
|
||||||
vertexList.y(quadStart + j, quad.y);
|
|
||||||
vertexList.z(quadStart + j, quad.z);
|
|
||||||
vertexList.normalX(quadStart + j, 0);
|
|
||||||
vertexList.normalY(quadStart + j, 0);
|
|
||||||
vertexList.normalZ(quadStart + j, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Vector4fc boundingSphere() {
|
|
||||||
return new Vector4f(0, 0, 0, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<GlyphModelKey, GlyphInstance> 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<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)
|
||||||
|
.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<Vector3f> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SignBlockEntity> implements SimpleDynamicVisual {
|
||||||
|
|
||||||
|
private static final Vec3 TEXT_OFFSET = new Vec3(0.0, 0.3333333432674408, 0.046666666865348816);
|
||||||
|
|
||||||
|
private static final ResourceReloadCache<WoodType, ModelTree> 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<FormattedCharSequence> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,9 @@ public class VanillaVisuals {
|
||||||
.factory(ShulkerBoxVisual::new)
|
.factory(ShulkerBoxVisual::new)
|
||||||
.apply();
|
.apply();
|
||||||
|
|
||||||
|
builder(BlockEntityType.SIGN).factory(SignVisual::new)
|
||||||
|
.apply();
|
||||||
|
|
||||||
builder(EntityType.CHEST_MINECART)
|
builder(EntityType.CHEST_MINECART)
|
||||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.CHEST_MINECART))
|
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.CHEST_MINECART))
|
||||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||||
|
|
Loading…
Reference in a new issue