Merge remote-tracking branch 'origin/1.20.1/text-visual' into 1.20.1/dev

# Conflicts:
#	common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl
#	common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java
#	common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java
#	common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java
#	common/src/vanillin/java/dev/engine_room/vanillin/visuals/SignVisual.java
#	forge/build.gradle.kts
This commit is contained in:
Jozufozu 2025-01-23 12:07:07 -06:00
commit 6003748f0e
33 changed files with 1540 additions and 10 deletions

View file

@ -116,6 +116,8 @@ jarSets {
dependencies {
modCompileOnly("net.fabricmc:fabric-loader:${property("fabric_loader_version")}")
compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!)
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
}

View file

@ -2,6 +2,8 @@ package dev.engine_room.flywheel.backend.engine;
import java.util.List;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.engine_room.flywheel.api.RenderContext;
import dev.engine_room.flywheel.api.backend.Engine;
import dev.engine_room.flywheel.api.instance.Instance;
@ -90,6 +92,8 @@ public class EngineImpl implements Engine {
@Override
public void setupRender(RenderContext context) {
try (var state = GlStateTracker.getRestoreState()) {
// Process the render queue for font updates
RenderSystem.replayQueue();
Uniforms.update(context);
environmentStorage.flush();
drawManager.flush(lightStorage, environmentStorage);

View file

@ -12,3 +12,5 @@ out vec3 flw_vertexNormal;
out float flw_distance;
FlwMaterial flw_material;
#define flw_vertexId gl_VertexID

View file

@ -2,7 +2,6 @@ package dev.engine_room.flywheel.lib.instance;
import dev.engine_room.flywheel.api.instance.InstanceHandle;
import dev.engine_room.flywheel.api.instance.InstanceType;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.util.FastColor;
public abstract class ColoredLitInstance extends AbstractInstance implements FlatLit {
@ -11,7 +10,6 @@ public abstract class ColoredLitInstance extends AbstractInstance implements Fla
public byte blue = (byte) 0xFF;
public byte alpha = (byte) 0xFF;
public int overlay = OverlayTexture.NO_OVERLAY;
public int light = 0;
public ColoredLitInstance(InstanceType<? extends ColoredLitInstance> type, InstanceHandle handle) {
@ -49,9 +47,12 @@ public abstract class ColoredLitInstance extends AbstractInstance implements Fla
return this;
}
public ColoredLitInstance overlay(int overlay) {
this.overlay = overlay;
return this;
public ColoredLitInstance color(float red, float green, float blue, float alpha) {
return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f), (byte) (alpha * 255f));
}
public ColoredLitInstance color(float red, float green, float blue) {
return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f));
}
@Override

View file

@ -0,0 +1,18 @@
package dev.engine_room.flywheel.lib.instance;
import dev.engine_room.flywheel.api.instance.InstanceHandle;
import dev.engine_room.flywheel.api.instance.InstanceType;
import net.minecraft.client.renderer.texture.OverlayTexture;
public abstract class ColoredLitOverlayInstance extends ColoredLitInstance {
public int overlay = OverlayTexture.NO_OVERLAY;
public ColoredLitOverlayInstance(InstanceType<? extends ColoredLitOverlayInstance> type, InstanceHandle handle) {
super(type, handle);
}
public ColoredLitOverlayInstance overlay(int overlay) {
this.overlay = overlay;
return this;
}
}

View file

@ -0,0 +1,68 @@
package dev.engine_room.flywheel.lib.instance;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import dev.engine_room.flywheel.api.instance.InstanceHandle;
import dev.engine_room.flywheel.api.instance.InstanceType;
import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import dev.engine_room.flywheel.lib.math.DataPacker;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
public class GlyphInstance extends ColoredLitInstance {
// Skew x by 1 - 0.25 * y
// Note that columns are written as rows.
private static final Matrix4fc ITALIC_SKEW = new Matrix4f(1, 0, 0, 0, -0.25f, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1);
public final Matrix4f pose = new Matrix4f();
public int packedUs;
public int packedVs;
public GlyphInstance(InstanceType<? extends GlyphInstance> type, InstanceHandle handle) {
super(type, handle);
}
public GlyphInstance setGlyph(BakedGlyph glyph, Matrix4fc initialPose, float x, float y, boolean italic) {
var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph);
setUvs(glyphExtension);
float left = glyphExtension.flywheel$left();
float up = glyphExtension.flywheel$up();
pose.set(initialPose);
pose.translate(x, y, 0.0f);
if (italic) {
pose.mul(ITALIC_SKEW);
}
pose.translate(left, up - 3.0f, 0.0f);
return this;
}
public GlyphInstance setEffect(BakedGlyph glyph, Matrix4fc initialPose, float x0, float y0, float x1, float y1, float depth) {
var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph);
setUvs(glyphExtension);
pose.set(initialPose);
pose.translate(x0, y0, depth);
pose.scale(x1 - x0, y1 - y0, 1.0f);
return this;
}
private void setUvs(BakedGlyphExtension glyphExtension) {
float u0 = glyphExtension.flywheel$u0();
float u1 = glyphExtension.flywheel$u1();
float v0 = glyphExtension.flywheel$v0();
float v1 = glyphExtension.flywheel$v1();
// Need to make sure at least u0/v0 don't get their sign bit extended in the cast.
// It causes u1/v1 to be completely saturated.
packedUs = (Short.toUnsignedInt(DataPacker.packNormU16(u1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(u0));
packedVs = (Short.toUnsignedInt(DataPacker.packNormU16(v1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(v0));
}
}

View file

@ -103,6 +103,27 @@ public final class InstanceTypes {
.cullShader(Flywheel.rl("instance/cull/shadow.glsl"))
.build();
public static final InstanceType<GlyphInstance> GLYPH = SimpleInstanceType.builder(GlyphInstance::new)
.layout(LayoutBuilder.create()
.matrix("pose", FloatRepr.FLOAT, 4)
.vector("u0u1v0v1", FloatRepr.NORMALIZED_UNSIGNED_SHORT, 4)
.vector("color", FloatRepr.NORMALIZED_UNSIGNED_BYTE, 4)
.vector("light", FloatRepr.UNSIGNED_SHORT, 2)
.build())
.writer((ptr, instance) -> {
ExtraMemoryOps.putMatrix4f(ptr, instance.pose);
ExtraMemoryOps.put2x16(ptr + 64, instance.packedUs);
ExtraMemoryOps.put2x16(ptr + 68, instance.packedVs);
MemoryUtil.memPutByte(ptr + 72, instance.red);
MemoryUtil.memPutByte(ptr + 73, instance.green);
MemoryUtil.memPutByte(ptr + 74, instance.blue);
MemoryUtil.memPutByte(ptr + 75, instance.alpha);
ExtraMemoryOps.put2x16(ptr + 76, instance.light);
})
.vertexShader(Flywheel.rl("instance/glyph.vert"))
.cullShader(Flywheel.rl("instance/cull/glyph.glsl"))
.build();
private InstanceTypes() {
}
}

View file

@ -10,7 +10,7 @@ import dev.engine_room.flywheel.lib.transform.Rotate;
import net.minecraft.core.Vec3i;
import net.minecraft.world.phys.Vec3;
public class OrientedInstance extends ColoredLitInstance implements Rotate<OrientedInstance> {
public class OrientedInstance extends ColoredLitOverlayInstance implements Rotate<OrientedInstance> {
public float posX;
public float posY;
public float posZ;

View file

@ -13,7 +13,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType;
import dev.engine_room.flywheel.lib.transform.Transform;
import net.minecraft.util.Mth;
public class PosedInstance extends ColoredLitInstance implements Transform<PosedInstance> {
public class PosedInstance extends ColoredLitOverlayInstance implements Transform<PosedInstance> {
public final Matrix4f pose = new Matrix4f();
public final Matrix3f normal = new Matrix3f();

View file

@ -12,7 +12,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType;
import dev.engine_room.flywheel.lib.transform.Affine;
import net.minecraft.core.Direction;
public class TransformedInstance extends ColoredLitInstance implements Affine<TransformedInstance> {
public class TransformedInstance extends ColoredLitOverlayInstance implements Affine<TransformedInstance> {
public final Matrix4f pose = new Matrix4f();
public TransformedInstance(InstanceType<? extends TransformedInstance> type, InstanceHandle handle) {

View file

@ -0,0 +1,25 @@
package dev.engine_room.flywheel.lib.internal;
import net.minecraft.resources.ResourceLocation;
public interface BakedGlyphExtension {
float flywheel$u0();
float flywheel$u1();
float flywheel$v0();
float flywheel$v1();
float flywheel$left();
float flywheel$right();
float flywheel$up();
float flywheel$down();
ResourceLocation flywheel$texture();
void flywheel$texture(ResourceLocation location);
}

View file

@ -10,7 +10,11 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.engine_room.flywheel.api.internal.DependencyInjection;
import dev.engine_room.flywheel.lib.transform.PoseTransformStack;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.resources.ResourceLocation;
public interface FlwLibLink {
FlwLibLink INSTANCE = DependencyInjection.load(FlwLibLink.class, "dev.engine_room.flywheel.impl.FlwLibLinkImpl");
@ -32,4 +36,10 @@ public interface FlwLibLink {
boolean isShaderPackInUse();
boolean isRenderingShadowPass();
FontSet getFontSet(Font font, ResourceLocation loc);
boolean getFilterFishyGlyphs(Font font);
BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph);
}

View file

@ -33,4 +33,32 @@ public final class DataPacker {
public static float unpackNormI8(byte b) {
return (float) b / 127f;
}
/**
* Pack a float as an unsigned, normalized short.
*/
public static short packNormU16(float f) {
return (short) (int) (Mth.clamp(f, 0.0f, 1.0f) * 65535);
}
/**
* Unpack an unsigned, normalized short to a float.
*/
public static float unpackNormU16(short s) {
return (float) (Short.toUnsignedInt(s)) / 65535f;
}
/**
* Pack a float as a signed, normalized byte.
*/
public static short packNormI16(float f) {
return (short) (Mth.clamp(f, -1.0f, 1.0f) * 32767);
}
/**
* Unpack a signed, normalized byte to a float.
*/
public static float unpackNormI16(short s) {
return (float) s / 32767f;
}
}

View file

@ -125,7 +125,7 @@ public final class FireComponent implements EntityComponent {
}
private record FireMesh(TextureAtlasSprite sprite) implements QuadMesh {
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, (float) (Math.sqrt(2) * 0.5));
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f);
@Override
public int vertexCount() {

View file

@ -0,0 +1,60 @@
package dev.engine_room.flywheel.lib.visual.text;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector2f;
import org.joml.Vector2fc;
public record SimpleTextLayer(GlyphPattern pattern, GlyphMaterial material, GlyphColor color, Vector2fc offset, int bias) implements TextLayer {
public static class Builder {
private static final Vector2fc NO_OFFSET = new Vector2f();
@Nullable
private GlyphPattern pattern;
@Nullable
private GlyphMaterial material;
@Nullable
private GlyphColor color;
private Vector2fc offset = NO_OFFSET;
private int bias = 0;
public Builder pattern(GlyphPattern pattern) {
this.pattern = pattern;
return this;
}
public Builder material(GlyphMaterial material) {
this.material = material;
return this;
}
public Builder color(GlyphColor color) {
this.color = color;
return this;
}
public Builder offset(Vector2fc offset) {
this.offset = offset;
return this;
}
public Builder offset(float offsetX, float offsetY) {
offset = new Vector2f(offsetX, offsetY);
return this;
}
public Builder bias(int bias) {
this.bias = bias;
return this;
}
public SimpleTextLayer build() {
Objects.requireNonNull(pattern);
Objects.requireNonNull(material);
Objects.requireNonNull(color);
return new SimpleTextLayer(pattern, material, color, offset, bias);
}
}
}

View file

@ -0,0 +1,217 @@
package dev.engine_room.flywheel.lib.visual.text;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector2f;
import org.joml.Vector2fc;
import dev.engine_room.flywheel.api.material.DepthTest;
import dev.engine_room.flywheel.api.material.Material;
import dev.engine_room.flywheel.api.material.Transparency;
import dev.engine_room.flywheel.api.material.WriteMask;
import dev.engine_room.flywheel.lib.material.CutoutShaders;
import dev.engine_room.flywheel.lib.material.FogShaders;
import dev.engine_room.flywheel.lib.material.SimpleMaterial;
import net.minecraft.client.gui.Font;
import net.minecraft.network.chat.TextColor;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FastColor;
public interface TextLayer {
/**
* The pattern of individual glyphs.
*
* @return A GlyphPattern.
*/
GlyphPattern pattern();
/**
* 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 offset of text in this layer.
*
* @return The offset.
*/
Vector2fc offset();
/**
* The instancer bias for this layer.
*
* @return The bias.
*/
int bias();
// TODO: probably just convert this to Iterable<Vector2fc>
@FunctionalInterface
interface GlyphPattern {
/**
* The pattern for a single glyph with no offset.
*/
GlyphPattern SINGLE = out -> out.accept(new Vector2f(0, 0));
/**
* The pattern for an 8x outline as used by glowing text on signs.
*/
GlyphPattern 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 Vector2f(x, y));
}
}
};
/**
* Add an arbitrary amount of glyphs. Each accepted vector represents
* the offset of a new glyph quad.
*
* @param out The consumer to accept the offset of a new glyph quad
*/
void addGlyphs(Consumer<Vector2fc> out);
}
@FunctionalInterface
interface GlyphMaterial {
// FIXME: account for intensity
GlyphMaterial NORMAL = texture -> SimpleMaterial.builder()
.cutout(CutoutShaders.ONE_TENTH)
.texture(texture)
.mipmap(false)
.transparency(Transparency.TRANSLUCENT)
.diffuse(false)
.build();
GlyphMaterial SEE_THROUGH = texture -> SimpleMaterial.builder()
.fog(FogShaders.NONE)
.cutout(CutoutShaders.ONE_TENTH)
.texture(texture)
.mipmap(false)
.depthTest(DepthTest.ALWAYS)
.transparency(Transparency.TRANSLUCENT)
.writeMask(WriteMask.COLOR)
.diffuse(false)
.build();
GlyphMaterial POLYGON_OFFSET = texture -> SimpleMaterial.builder()
.cutout(CutoutShaders.ONE_TENTH)
.texture(texture)
.mipmap(false)
.polygonOffset(true)
.transparency(Transparency.TRANSLUCENT)
.diffuse(false)
.build();
static GlyphMaterial fromDisplayMode(Font.DisplayMode displayMode) {
return switch (displayMode) {
case NORMAL -> NORMAL;
case SEE_THROUGH -> SEE_THROUGH;
case POLYGON_OFFSET -> POLYGON_OFFSET;
};
}
/**
* Create a Flywheel material for the given glyph texture.
*
* @param texture The texture to use.
* @return A material.
*/
Material create(ResourceLocation texture);
}
@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, float dimFactor) {
int finalColor;
if (dimFactor != 1.0f) {
finalColor = FastColor.ARGB32.color(
FastColor.ARGB32.alpha(color),
(int) (FastColor.ARGB32.red(color) * dimFactor),
(int) (FastColor.ARGB32.green(color) * dimFactor),
(int) (FastColor.ARGB32.blue(color) * dimFactor)
);
} else {
finalColor = color;
}
return textColor -> {
if (textColor != null) {
int textColorArgb = textColor.getValue();
if (dimFactor != 1.0f) {
return FastColor.ARGB32.color(
FastColor.ARGB32.alpha(finalColor),
(int) (FastColor.ARGB32.red(textColorArgb) * dimFactor),
(int) (FastColor.ARGB32.green(textColorArgb) * dimFactor),
(int) (FastColor.ARGB32.blue(textColorArgb) * dimFactor)
);
} else {
return (finalColor & 0xFF000000) | (textColorArgb & 0xFFFFFF);
}
}
return finalColor;
};
}
/**
* 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 defaultTo(color, 1.0f);
}
/**
* Always use the given color, regardless of the style.
*
* @param color The ARGB color to use.
* @return A new GlyphColor.
*/
static GlyphColor always(int color) {
return textColor -> color;
}
/**
* 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 nullable text color to a color.
*
* @param textColor The color of the text to colorize.
* @return The color to use, in ARGB format.
*/
int color(@Nullable TextColor textColor);
}
}

View file

@ -0,0 +1,45 @@
package dev.engine_room.flywheel.lib.visual.text;
import net.minecraft.client.gui.Font;
public final class TextLayers {
public static TextLayer normal(int color, Font.DisplayMode displayMode, int bias) {
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE)
.material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode))
.color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color)))
.bias(bias)
.build();
}
public static TextLayer normal(int color, Font.DisplayMode displayMode) {
return normal(color, displayMode, 0);
}
public static TextLayer dropShadow(int color, Font.DisplayMode displayMode, int bias) {
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE)
.material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode))
.color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color), 0.25f))
.offset(1, 1)
.bias(bias)
.build();
}
public static TextLayer dropShadow(int color, Font.DisplayMode displayMode) {
return dropShadow(color, displayMode, 0);
}
public static TextLayer outline(int color, int bias) {
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.OUTLINE)
.material(TextLayer.GlyphMaterial.NORMAL)
.color(TextLayer.GlyphColor.always(TextLayer.GlyphColor.adjustColor(color)))
.bias(bias)
.build();
}
public static TextLayer outline(int color) {
return outline(color, 0);
}
private TextLayers() {
}
}

View file

@ -0,0 +1,380 @@
package dev.engine_room.flywheel.lib.visual.text;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector2fc;
import org.joml.Vector4f;
import org.joml.Vector4fc;
import com.mojang.blaze3d.font.GlyphInfo;
import dev.engine_room.flywheel.api.instance.InstancerProvider;
import dev.engine_room.flywheel.api.model.Mesh;
import dev.engine_room.flywheel.api.model.Model;
import dev.engine_room.flywheel.api.vertex.MutableVertexList;
import dev.engine_room.flywheel.lib.instance.GlyphInstance;
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.texture.OverlayTexture;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.util.FormattedCharSink;
import net.minecraft.util.Mth;
/**
* A visual that renders a single line of text.
*/
public final class TextVisual {
private static final Font FONT = Minecraft.getInstance().font;
private static final ResourceReloadCache<GlyphMeshKey, GlyphMesh> GLYPH_MESH_CACHE = new ResourceReloadCache<>(GlyphMeshKey::into);
private static final ResourceReloadCache<GlyphModelKey, Model> GLYPH_MODEL_CACHE = new ResourceReloadCache<>(GlyphModelKey::into);
private static final ThreadLocal<Sink> SINKS = ThreadLocal.withInitial(Sink::new);
private final SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler;
private final List<TextLayer> layers = new ArrayList<>();
private final Matrix4f pose = new Matrix4f();
private FormattedCharSequence text = FormattedCharSequence.EMPTY;
private int backgroundColor = 0;
private int light;
public TextVisual(InstancerProvider provider) {
recycler = new SmartRecycler<>(key -> provider.instancer(InstanceTypes.GLYPH, GLYPH_MODEL_CACHE.get(key.modelKey), key.bias)
.createInstance());
}
public void setup(FormattedCharSequence textLine, List<TextLayer> layers, Matrix4f pose, int light) {
// TODO: probably don't store everything
this.text = textLine;
this.layers.clear();
this.layers.addAll(layers);
this.pose.set(pose);
this.light = light;
setup();
}
public void updateObfuscated() {
// TODO: track obfuscated glyphs and update here
setup();
}
public void backgroundColor(int backgroundColor) {
// TODO: don't setup the whole thing
this.backgroundColor = backgroundColor;
setup();
}
public void updateLight(int packedLight) {
// TODO: just iterate over instances and update light
light = packedLight;
setup();
}
private void setup() {
recycler.resetCount();
var sink = SINKS.get();
sink.prepare(recycler, layers, pose, light);
text.accept(sink);
sink.addBackground(backgroundColor, 0, sink.x);
sink.clear();
recycler.discardExtra();
}
private TextVisual reset() {
// TODO: should this be public? what should it do?
layers.clear();
pose.identity();
text = FormattedCharSequence.EMPTY;
backgroundColor = 0;
light = 0;
return this;
}
public void delete() {
recycler.delete();
}
private record GlyphMeshKey(float glyphWidth, float glyphHeight, TextLayer.GlyphPattern pattern, boolean bold, float boldOffset, float shadowOffset) {
public GlyphMesh into() {
List<Vector2fc> out = new ArrayList<>();
pattern.addGlyphs(offsetc -> {
Vector2f offset = new Vector2f(offsetc).mul(shadowOffset);
out.add(offset);
if (bold) {
out.add(new Vector2f(offset.x() + boldOffset, offset.y()));
}
});
return new GlyphMesh(glyphWidth, glyphHeight, out.toArray(Vector2fc[]::new));
}
}
private record GlyphModelKey(@Nullable GlyphMeshKey meshKey, TextLayer.GlyphMaterial material, ResourceLocation texture) {
public Model into() {
Mesh mesh;
if (meshKey != null) {
mesh = GLYPH_MESH_CACHE.get(meshKey);
} else {
mesh = GlyphEffectMesh.INSTANCE;
}
return new SingleMeshModel(mesh, material.create(texture));
}
}
private record GlyphInstanceKey(GlyphModelKey modelKey, int bias) {
}
private static class Sink implements FormattedCharSink {
@UnknownNullability
private SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler;
@UnknownNullability
private List<TextLayer> layers;
@UnknownNullability
private Matrix4f pose;
private int light;
private float x;
public void prepare(SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler, List<TextLayer> layers, Matrix4f pose, int light) {
this.recycler = recycler;
this.layers = layers;
this.pose = pose;
this.light = light;
this.x = 0;
}
public void clear() {
recycler = null;
layers = null;
pose = null;
}
@Override
public boolean accept(int index, Style style, int codePoint) {
FontSet fontSet = FlwLibLink.INSTANCE.getFontSet(FONT, style.getFont());
GlyphInfo glyphInfo = fontSet.getGlyphInfo(codePoint, FlwLibLink.INSTANCE.getFilterFishyGlyphs(FONT));
BakedGlyph glyph = style.isObfuscated() && codePoint != ' ' ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(codePoint);
boolean bold = style.isBold();
float advance = glyphInfo.getAdvance(bold);
// Process layers in the inner loop for 2 reasons:
// 1. So we don't have to iterate over all the text and to the same glyph lookups for each layer
// 2. So we get the same random draw in each layer for obfuscated text
for (TextLayer layer : layers) {
int color = layer.color()
.color(style.getColor());
Vector2fc offset = layer.offset();
if (!(glyph instanceof EmptyGlyph)) {
GlyphInstance instance = recycler.get(key(layer, glyphInfo, glyph, bold));
float shadowOffset = glyphInfo.getShadowOffset();
instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, offset.y() * shadowOffset, style.isItalic());
instance.colorArgb(color);
instance.light(light);
instance.setChanged();
}
// SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer.
if (style.isStrikethrough()) {
addEffect(layer, x + offset.x() - 1.0f, offset.y() + 4.5f, x + offset.x() + advance, offset.y() + 4.5f - 1.0f, 0.01f, color);
}
if (style.isUnderlined()) {
addEffect(layer, x + offset.x() - 1.0f, offset.y() + 9.0f, x + offset.x() + advance, offset.y() + 9.0f - 1.0f, 0.01f, color);
}
}
x += advance;
return true;
}
public void addBackground(int backgroundColor, float startX, float endX) {
if (backgroundColor != 0) {
BakedGlyph glyph = FlwLibLink.INSTANCE.getFontSet(FONT, Style.DEFAULT_FONT)
.whiteGlyph();
var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph);
GlyphInstance instance = recycler.get(effectKey(glyphExtension.flywheel$texture(), TextLayer.GlyphMaterial.SEE_THROUGH, 0));
instance.setEffect(glyph, pose, startX - 1.0f, 9.0f, endX + 1.0f, 1.0f, 0.01f);
instance.colorArgb(backgroundColor);
instance.light(light);
instance.setChanged();
}
}
private void addEffect(TextLayer layer, float x0, float y0, float x1, float y1, float depth, int colorArgb) {
BakedGlyph glyph = FlwLibLink.INSTANCE.getFontSet(FONT, Style.DEFAULT_FONT)
.whiteGlyph();
GlyphInstance instance = recycler.get(effectKey(glyph, layer));
instance.setEffect(glyph, pose, x0, y0, x1, y1, depth);
instance.colorArgb(colorArgb);
instance.light(light);
instance.setChanged();
}
private static GlyphInstanceKey key(TextLayer layer, GlyphInfo glyphInfo, BakedGlyph glyph, boolean bold) {
var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph);
float glyphWidth = glyphExtension.flywheel$right() - glyphExtension.flywheel$left();
float glyphHeight = glyphExtension.flywheel$down() - glyphExtension.flywheel$up();
return key(layer, glyphWidth, glyphHeight, glyphExtension.flywheel$texture(), bold, bold ? glyphInfo.getBoldOffset() : 0, glyphInfo.getShadowOffset());
}
private static GlyphInstanceKey key(TextLayer layer, float glyphWidth, float glyphHeight, ResourceLocation texture, boolean bold, float boldOffset, float shadowOffset) {
var meshKey = new GlyphMeshKey(glyphWidth, glyphHeight, layer.pattern(), bold, boldOffset, shadowOffset);
var modelKey = new GlyphModelKey(meshKey, layer.material(), texture);
return new GlyphInstanceKey(modelKey, layer.bias());
}
private static GlyphInstanceKey effectKey(BakedGlyph glyph, TextLayer layer) {
var glyphExtension = FlwLibLink.INSTANCE.getBakedGlyphExtension(glyph);
return effectKey(glyphExtension.flywheel$texture(), layer.material(), layer.bias());
}
private static GlyphInstanceKey effectKey(ResourceLocation texture, TextLayer.GlyphMaterial material, int bias) {
var modelKey = new GlyphModelKey(null, material, texture);
return new GlyphInstanceKey(modelKey, bias);
}
}
/**
* A mesh that represents a pattern of a glyph with a certain width and height. Expects to be drawn with the glyph
* instance type.
*
* @param offsets Each offset will be expanded into a glyph quad.
*/
private record GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets, Vector4fc boundingSphere) implements QuadMesh {
private static final float[] X = new float[] { 0, 0, 1, 1 };
private static final float[] Y = new float[] { 0, 1, 1, 0 };
public GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets) {
this(glyphWidth, glyphHeight, offsets, boundingSphere(glyphWidth, glyphHeight, offsets));
}
@Override
public int vertexCount() {
return 4 * offsets.length;
}
@Override
public void write(MutableVertexList vertexList) {
for (int i = 0; i < offsets.length; i++) {
Vector2fc offset = offsets[i];
var startVertex = i * 4;
for (int j = 0; j < 4; j++) {
vertexList.x(startVertex + j, offset.x() + (glyphWidth * X[j]));
vertexList.y(startVertex + j, offset.y() + (glyphHeight * Y[j]));
vertexList.z(startVertex + j, 0);
vertexList.r(startVertex + j, 1);
vertexList.g(startVertex + j, 1);
vertexList.b(startVertex + j, 1);
vertexList.a(startVertex + j, 1);
vertexList.overlay(startVertex + j, OverlayTexture.NO_OVERLAY);
vertexList.normalX(startVertex + j, 0);
vertexList.normalY(startVertex + j, 0);
vertexList.normalZ(startVertex + j, 1);
}
}
}
@Override
public Vector4fc boundingSphere() {
return boundingSphere;
}
private static Vector4fc boundingSphere(float glyphWidth, float glyphHeight, Vector2fc[] offsets) {
if (offsets.length == 0) {
return new Vector4f(0, 0, 0, 0);
}
float minX = Float.POSITIVE_INFINITY;
float minY = Float.POSITIVE_INFINITY;
float maxX = Float.NEGATIVE_INFINITY;
float maxY = Float.NEGATIVE_INFINITY;
for (Vector2fc offset : offsets) {
for (int j = 0; j < 4; j++) {
var x = offset.x() + (glyphWidth * X[j]);
var y = offset.y() + (glyphHeight * Y[j]);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
float x = (minX + maxX) / 2;
float y = (minY + maxY) / 2;
float sizeX = maxX - minX;
float sizeY = maxY - minY;
float maxSize = Math.max(sizeX, sizeY);
return new Vector4f(x, y, 0, Mth.SQRT_OF_TWO * maxSize / 2);
}
}
private record GlyphEffectMesh() implements QuadMesh {
private static final float[] X = new float[] { 0, 1, 1, 0 };
private static final float[] Y = new float[] { 0, 0, 1, 1 };
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0.5f, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f);
public static final GlyphEffectMesh INSTANCE = new GlyphEffectMesh();
@Override
public int vertexCount() {
return 4;
}
@Override
public void write(MutableVertexList vertexList) {
for (int i = 0; i < 4; i++) {
vertexList.x(i, X[i]);
vertexList.y(i, Y[i]);
vertexList.z(i, 0);
vertexList.r(i, 1);
vertexList.g(i, 1);
vertexList.b(i, 1);
vertexList.a(i, 1);
vertexList.normalX(i, 0);
vertexList.normalY(i, 0);
vertexList.normalZ(i, 1);
}
}
@Override
public Vector4fc boundingSphere() {
return BOUNDING_SPHERE;
}
}
}

View file

@ -0,0 +1,5 @@
#include "flywheel:util/matrix.glsl"
void flw_transformBoundingSphere(in FlwInstance i, inout vec3 center, inout float radius) {
transformBoundingSphere(i.pose, center, radius);
}

View file

@ -0,0 +1,15 @@
void flw_instanceVertex(in FlwInstance i) {
uint vertexInGlyph = flw_vertexId % 4;
uint yIndex = ((vertexInGlyph + 1u) >> 1u) & 1u;
flw_vertexPos = i.pose * flw_vertexPos;
flw_vertexTexCoord.s = i.u0u1v0v1[(vertexInGlyph & 2u) >> 1u];
flw_vertexTexCoord.t = i.u0u1v0v1[2u + yIndex];
flw_vertexColor *= i.color;
// Some drivers have a bug where uint over float division is invalid, so use an explicit cast.
flw_vertexLight = vec2(i.light) / 256.0;
}

View file

@ -13,9 +13,15 @@ import dev.engine_room.flywheel.impl.compat.OptifineCompat;
import dev.engine_room.flywheel.impl.extension.PoseStackExtension;
import dev.engine_room.flywheel.impl.mixin.ModelPartAccessor;
import dev.engine_room.flywheel.impl.mixin.PoseStackAccessor;
import dev.engine_room.flywheel.impl.mixin.text.FontAccessor;
import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension;
import dev.engine_room.flywheel.lib.internal.FlwLibLink;
import dev.engine_room.flywheel.lib.transform.PoseTransformStack;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.resources.ResourceLocation;
public class FlwLibLinkImpl implements FlwLibLink {
@Override
@ -74,4 +80,19 @@ public class FlwLibLinkImpl implements FlwLibLink {
return false;
}
}
@Override
public FontSet getFontSet(Font font, ResourceLocation loc) {
return ((FontAccessor) font).flywheel$getFontSet(loc);
}
@Override
public boolean getFilterFishyGlyphs(Font font) {
return ((FontAccessor) font).flywheel$getFilterFishyGlyphs();
}
@Override
public BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph) {
return (BakedGlyphExtension) glyph;
}
}

View file

@ -0,0 +1,10 @@
package dev.engine_room.flywheel.impl;
import com.mojang.blaze3d.font.SheetGlyphInfo;
/**
* For use in {@link dev.engine_room.flywheel.impl.mixin.text.FontTextureMixin}
* to batch glyph uploads when they're created in a flywheel worker thread.
*/
public record FontTextureUpload(SheetGlyphInfo info, int x, int y) {
}

View file

@ -0,0 +1,7 @@
package dev.engine_room.flywheel.impl.extension;
import net.minecraft.resources.ResourceLocation;
public interface FontTextureExtension {
void flywheel$setName(ResourceLocation name);
}

View file

@ -0,0 +1,91 @@
package dev.engine_room.flywheel.impl.mixin.text;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.resources.ResourceLocation;
@Mixin(BakedGlyph.class)
public class BakedGlyphMixin implements BakedGlyphExtension {
@Shadow
@Final
private float u0;
@Shadow
@Final
private float u1;
@Shadow
@Final
private float v0;
@Shadow
@Final
private float v1;
@Shadow
@Final
private float left;
@Shadow
@Final
private float right;
@Shadow
@Final
private float up;
@Shadow
@Final
private float down;
@Unique
private ResourceLocation flywheel$texture;
@Override
public float flywheel$u0() {
return u0;
}
@Override
public float flywheel$u1() {
return u1;
}
@Override
public float flywheel$v0() {
return v0;
}
@Override
public float flywheel$v1() {
return v1;
}
@Override
public float flywheel$left() {
return left;
}
@Override
public float flywheel$right() {
return right;
}
@Override
public float flywheel$up() {
return up;
}
@Override
public float flywheel$down() {
return down;
}
@Override
public ResourceLocation flywheel$texture() {
return flywheel$texture;
}
@Override
public void flywheel$texture(ResourceLocation location) {
flywheel$texture = location;
}
}

View file

@ -0,0 +1,59 @@
package dev.engine_room.flywheel.impl.mixin.text;
import java.util.function.IntFunction;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.client.gui.font.CodepointMap;
@Mixin(CodepointMap.class)
public class CodePointMapMixin<T> {
@Unique
private final Object flywheel$lock = new Object();
@WrapMethod(method = "clear")
private void flywheel$wrapClearAsSynchronized(Operation<Void> original) {
synchronized (flywheel$lock) {
original.call();
}
}
@WrapMethod(method = "get")
private T flywheel$wrapGetAsSynchronized(int index, Operation<T> original) {
synchronized (flywheel$lock) {
return original.call(index);
}
}
@WrapMethod(method = "put")
private T flywheel$wrapPutAsSynchronized(int index, T value, Operation<T> original) {
synchronized (flywheel$lock) {
return original.call(index, value);
}
}
@WrapMethod(method = "computeIfAbsent")
private T flywheel$wrapComputeIfAbsentAsSynchronized(int index, IntFunction<T> valueIfAbsentGetter, Operation<T> original) {
synchronized (flywheel$lock) {
return original.call(index, valueIfAbsentGetter);
}
}
@WrapMethod(method = "remove")
private T flywheel$wrapRemoveAsSynchronized(int index, Operation<T> original) {
synchronized (flywheel$lock) {
return original.call(index);
}
}
@WrapMethod(method = "forEach")
private void flywheel$wrapForEachAsSynchronized(CodepointMap.Output<T> output, Operation<Void> original) {
synchronized (flywheel$lock) {
original.call(output);
}
}
}

View file

@ -0,0 +1,18 @@
package dev.engine_room.flywheel.impl.mixin.text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.resources.ResourceLocation;
@Mixin(Font.class)
public interface FontAccessor {
@Accessor("filterFishyGlyphs")
boolean flywheel$getFilterFishyGlyphs();
@Invoker("getFontSet")
FontSet flywheel$getFontSet(ResourceLocation fontLocation);
}

View file

@ -0,0 +1,31 @@
package dev.engine_room.flywheel.impl.mixin.text;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import dev.engine_room.flywheel.impl.extension.FontTextureExtension;
import net.minecraft.client.gui.font.FontSet;
import net.minecraft.client.gui.font.FontTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
@Mixin(FontSet.class)
public abstract class FontSetMixin {
// Replace serial random with thread-local random
@Shadow
@Final
private static RandomSource RANDOM = RandomSource.createNewThreadLocalInstance();
@ModifyExpressionValue(method = "stitch", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/FontTexture"))
private FontTexture flywheel$setNameAfterCreate(FontTexture original, @Local ResourceLocation name) {
// Forward the name to the FontTexture so we can forward the name to the BakedGlyphs it creates.
// We need to know that to determine which Material to use when actually setting up instances.
((FontTextureExtension) original).flywheel$setName(name);
return original;
}
}

View file

@ -0,0 +1,13 @@
package dev.engine_room.flywheel.impl.mixin.text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(targets = "net.minecraft.client.gui.font.FontTexture$Node")
public interface FontTexture$NodeAccessor {
@Accessor("x")
int flywheel$getX();
@Accessor("y")
int flywheel$getY();
}

View file

@ -0,0 +1,114 @@
package dev.engine_room.flywheel.impl.mixin.text;
import java.util.ArrayList;
import java.util.List;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import com.mojang.blaze3d.font.SheetGlyphInfo;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.engine_room.flywheel.impl.FontTextureUpload;
import dev.engine_room.flywheel.impl.extension.FontTextureExtension;
import dev.engine_room.flywheel.lib.internal.BakedGlyphExtension;
import net.minecraft.client.gui.font.FontTexture;
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.resources.ResourceLocation;
@Mixin(FontTexture.class)
public abstract class FontTextureMixin extends AbstractTexture implements FontTextureExtension {
@Unique
private final List<FontTextureUpload> flywheel$uploads = new ArrayList<>();
@Unique
private boolean flywheel$flushScheduled = false;
@Unique
private ResourceLocation flywheel$name;
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;getId()I"))
private int flywheel$skipGetId(FontTexture instance, Operation<Integer> original) {
// getId lazily creates the texture id, which is good,
// but it doesn't check for the render thread, which explodes.
if (RenderSystem.isOnRenderThreadOrInit()) {
return original.call(instance);
}
// We'll call getId manually in the recorded render call below.
return 0;
}
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/TextureUtil;prepareImage(Lcom/mojang/blaze3d/platform/NativeImage$InternalGlFormat;III)V"))
private void flywheel$skipPrepareImage(NativeImage.InternalGlFormat arg, int i, int j, int k, Operation<Void> original) {
if (RenderSystem.isOnRenderThreadOrInit()) {
original.call(arg, i, j, k);
} else {
RenderSystem.recordRenderCall(() -> original.call(arg, getId(), j, k));
}
}
@WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;bind()V"))
private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload(FontTexture instance) {
return RenderSystem.isOnRenderThreadOrInit();
}
@WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V"))
private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload2(SheetGlyphInfo instance, int x, int y) {
return RenderSystem.isOnRenderThreadOrInit();
}
@WrapOperation(method = "add", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/font/FontTexture$Node;x:I", ordinal = 0))
private int flywheel$shareNode(@Coerce Object instance, Operation<Integer> original, @Share("node") LocalRef<Object> node) {
node.set(instance);
return original.call(instance);
}
@Inject(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V", shift = At.Shift.AFTER))
private void flywheel$uploadOrFlush(SheetGlyphInfo glyphInfo, CallbackInfoReturnable<BakedGlyph> cir, @Share("node") LocalRef<Object> node) {
FontTexture$NodeAccessor accessor = ((FontTexture$NodeAccessor) node.get());
// Shove all the uploads into a list to be processed as a batch.
// Saves a lot of lambda allocations that would be spent binding the same texture over and over.
flywheel$uploads.add(new FontTextureUpload(glyphInfo, accessor.flywheel$getX(), accessor.flywheel$getY()));
if (!flywheel$flushScheduled) {
RenderSystem.recordRenderCall(this::flywheel$flush);
flywheel$flushScheduled = true;
}
}
@ModifyExpressionValue(method = "add", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/glyphs/BakedGlyph"))
private BakedGlyph flywheel$setGlyphExtensionName(BakedGlyph original) {
((BakedGlyphExtension) original).flywheel$texture(flywheel$name);
return original;
}
@Unique
public void flywheel$flush() {
this.bind();
for (FontTextureUpload upload : flywheel$uploads) {
upload.info()
.upload(upload.x(), upload.y());
}
flywheel$uploads.clear();
flywheel$flushScheduled = false;
}
@Override
public void flywheel$setName(ResourceLocation name) {
flywheel$name = name;
}
}

View file

@ -16,6 +16,12 @@
"PoseStackMixin",
"fix.FixFabulousDepthMixin",
"fix.FixNormalScalingMixin",
"text.BakedGlyphMixin",
"text.CodePointMapMixin",
"text.FontAccessor",
"text.FontSetMixin",
"text.FontTexture$NodeAccessor",
"text.FontTextureMixin",
"visualmanage.BlockEntityMixin",
"visualmanage.LevelChunkMixin",
"visualmanage.LevelRendererMixin",

View file

@ -0,0 +1,256 @@
package dev.engine_room.flywheel.vanilla;
import java.util.ArrayList;
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.text.TextLayer;
import dev.engine_room.flywheel.lib.visual.text.TextLayers;
import dev.engine_room.flywheel.lib.visual.text.TextVisual;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.util.FastColor;
import net.minecraft.util.FormattedCharSequence;
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 Font FONT = Minecraft.getInstance().font;
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 InstanceTree instances;
private final Matrix4f initialPose;
// The 8 lines of text we render
private final TextVisual[] frontTextVisuals = new TextVisual[4];
private final TextVisual[] backTextVisuals = new TextVisual[4];
// Need to update these every frame, so just remember which ones are obfuscated
// Most of the time this will be empty.
private final List<TextVisual> obfuscated = new ArrayList<>();
private int packedLight = 0;
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++) {
frontTextVisuals[i] = new TextVisual(ctx.instancerProvider());
backTextVisuals[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 visualPosition = getVisualPosition();
var signModelRenderScale = getSignModelRenderScale();
var rotation = -block.getYRotationDegrees(blockState);
initialPose = new Matrix4f().translate(visualPosition.getX() + 0.5f, visualPosition.getY() + 0.75f * signModelRenderScale, visualPosition.getZ() + 0.5f)
.rotateY(Mth.DEG_TO_RAD * rotation);
if (!isStanding) {
initialPose.translate(0.0f, -0.3125f, -0.4375f);
}
// Only apply this to the instances because text gets a separate scaling.
Matrix4f initialModelPose = new Matrix4f(initialPose).scale(signModelRenderScale, -signModelRenderScale, -signModelRenderScale);
instances.updateInstancesStatic(initialModelPose);
lastFrontText = blockEntity.getFrontText();
lastBackText = blockEntity.getBackText();
setupText(lastFrontText, true);
setupText(lastBackText, false);
}
private static ModelTree createSignModel(WoodType woodType) {
return ModelTrees.of(ModelLayers.createSignModelName(woodType), Sheets.getSignMaterial(woodType), MATERIAL);
}
@Override
public void beginFrame(Context ctx) {
boolean doSetup = false;
if (lastFrontText != blockEntity.getFrontText()) {
lastFrontText = blockEntity.getFrontText();
doSetup = true;
}
if (lastBackText != blockEntity.getBackText()) {
lastBackText = blockEntity.getBackText();
doSetup = true;
}
if (doSetup) {
// Setup both to make it easier to track obfuscation
obfuscated.clear();
setupText(lastFrontText, true);
setupText(lastBackText, false);
} else {
// The is visible check is relatively expensive compared to the boolean checks above,
// so only do it when it'll actually save some work in obfuscating.
if (isVisible(ctx.frustum())) {
obfuscated.forEach(TextVisual::updateObfuscated);
}
}
}
@Override
public void updateLight(float partialTick) {
packedLight = computePackedLight();
instances.traverse(instance -> {
instance.light(packedLight)
.setChanged();
});
if (!lastFrontText.hasGlowingText()) {
for (var text : frontTextVisuals) {
text.updateLight(packedLight);
}
}
if (!lastBackText.hasGlowingText()) {
for (var text : backTextVisuals) {
text.updateLight(packedLight);
}
}
}
@Override
public void collectCrumblingInstances(Consumer<@Nullable Instance> consumer) {
instances.traverse(consumer);
}
@Override
protected void _delete() {
instances.delete();
for (var text : frontTextVisuals) {
text.delete();
}
for (var text : backTextVisuals) {
text.delete();
}
}
protected float getSignModelRenderScale() {
return 0.6666667f;
}
protected float getSignTextRenderScale() {
return 0.6666667f;
}
protected Vec3 getTextOffset() {
return TEXT_OFFSET;
}
private void setupText(SignText text, boolean isFrontText) {
FormattedCharSequence[] textLines = text.getRenderMessages(Minecraft.getInstance()
.isTextFilteringEnabled(), component -> {
List<FormattedCharSequence> list = FONT.split(component, blockEntity.getMaxTextLineWidth());
return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0);
});
List<TextLayer> layers = new ArrayList<>();
int darkColor = getDarkColor(text);
int textColor;
if (text.hasGlowingText()) {
textColor = text.getColor()
.getTextColor();
layers.add(TextLayers.outline(darkColor));
} else {
textColor = darkColor;
}
layers.add(TextLayers.normal(textColor, Font.DisplayMode.POLYGON_OFFSET, 1));
var textVisuals = isFrontText ? frontTextVisuals : backTextVisuals;
int lineHeight = blockEntity.getTextLineHeight();
int lineDelta = 4 * lineHeight / 2;
for (int i = 0; i < 4; ++i) {
FormattedCharSequence textLine = textLines[i];
float x = (float) (-FONT.width(textLine) / 2);
float y = i * lineHeight - lineDelta;
var pose = new Matrix4f(initialPose);
if (!isFrontText) {
pose.rotateY(Mth.PI);
}
float scale = 0.015625f * getSignTextRenderScale();
var textOffset = getTextOffset();
pose.translate((float) textOffset.x, (float) textOffset.y, (float) textOffset.z);
pose.scale(scale, -scale, scale);
pose.translate(x, y, 0.0f);
var textVisual = textVisuals[i];
int light = text.hasGlowingText() ? LightTexture.FULL_BRIGHT : packedLight;
textVisual.setup(textLine, layers, pose, light);
if (hasObfuscation(textLine)) {
obfuscated.add(textVisual);
}
}
}
private static int getDarkColor(SignText signText) {
int colorArgb = signText.getColor()
.getTextColor();
if (colorArgb == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) {
return 0xFFF0EBCC;
}
int r = (int) ((double) FastColor.ARGB32.red(colorArgb) * 0.4);
int g = (int) ((double) FastColor.ARGB32.green(colorArgb) * 0.4);
int b = (int) ((double) FastColor.ARGB32.blue(colorArgb) * 0.4);
return FastColor.ARGB32.color(0, r, g, b);
}
private static boolean hasObfuscation(FormattedCharSequence text) {
return text.accept((i, s, j) -> s.isObfuscated());
}
}

View file

@ -113,6 +113,9 @@ dependencies {
modCompileOnly("maven.modrinth:embeddium:${property("embeddium_version")}")
compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!)
implementation(include("io.github.llamalad7:mixinextras-forge:0.4.1")!!)
"forApi"(project(path = common, configuration = "apiClasses"))
"forLib"(project(path = common, configuration = "libClasses"))
"forBackend"(project(path = common, configuration = "backendClasses"))

View file

@ -35,7 +35,7 @@ parchment_version = 2023.09.03
# Minecraft build dependency versions
minecraft_version = 1.20.1
forge_version = 47.2.19
fabric_loader_version = 0.15.9
fabric_loader_version = 0.16.5
fabric_api_version = 0.92.1+1.20.1
# Build dependency mod versions