diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 23d1f23b4..85e8f9878 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -72,6 +72,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") } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java index 3a84e3a13..47b58fae5 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java @@ -2,6 +2,8 @@ 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 void onLightUpdate(SectionPos sectionPos, LightLayer layer) { @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); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert index d1e751929..019df0083 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert @@ -12,3 +12,5 @@ out vec3 flw_vertexNormal; out float flw_distance; FlwMaterial flw_material; + +#define flw_vertexId gl_VertexID diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl index 395979d5c..2817e4b69 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/cull.glsl @@ -87,38 +87,46 @@ bool _flw_isVisible(uint instanceIndex, uint modelIndex) { transformBoundingSphere(flw_view, center, radius); vec4 aabb; - if (projectSphere(center, radius, _flw_cullData.znear, _flw_cullData.P00, _flw_cullData.P11, aabb)) - { - float width = (aabb.z - aabb.x) * _flw_cullData.pyramidWidth; - float height = (aabb.w - aabb.y) * _flw_cullData.pyramidHeight; + if (projectSphere(center, radius, _flw_cullData.znear, _flw_cullData.P00, _flw_cullData.P11, aabb)) { + vec2 size = aabb.zw - aabb.xy; - int level = clamp(int(ceil(log2(max(width, height)))), 0, _flw_cullData.pyramidLevels); + vec2 sizeInPixels = size * flw_viewportSize; - ivec2 levelSize = textureSize(_flw_depthPyramid, level); + // Cull objects that are less than a pixel in size. Could probably make this configurable. + isVisible = isVisible && any(greaterThan(sizeInPixels, vec2(1.))); - ivec4 levelSizePair = ivec4(levelSize, levelSize); + if (isVisible) { + float width = size.x * _flw_cullData.pyramidWidth; + float height = size.y * _flw_cullData.pyramidHeight; - ivec4 bounds = ivec4(aabb * vec4(levelSizePair)); + int level = clamp(int(ceil(log2(max(width, height)))), 0, _flw_cullData.pyramidLevels); - // Clamp to the texture bounds. - // Since we're not going through a sampler out of bounds texel fetches will return 0. - bounds = clamp(bounds, ivec4(0), levelSizePair); + ivec2 levelSize = textureSize(_flw_depthPyramid, level); - float depth01 = texelFetch(_flw_depthPyramid, bounds.xw, level).r; - float depth11 = texelFetch(_flw_depthPyramid, bounds.zw, level).r; - float depth10 = texelFetch(_flw_depthPyramid, bounds.zy, level).r; - float depth00 = texelFetch(_flw_depthPyramid, bounds.xy, level).r; + ivec4 levelSizePair = ivec4(levelSize, levelSize); - float depth; - if (_flw_cullData.useMin == 0) { - depth = max(max(depth00, depth01), max(depth10, depth11)); - } else { - depth = min(min(depth00, depth01), min(depth10, depth11)); - } + ivec4 bounds = ivec4(aabb * vec4(levelSizePair)); + + // Clamp to the texture bounds. + // Since we're not going through a sampler out of bounds texel fetches will return 0. + bounds = clamp(bounds, ivec4(0), levelSizePair); + + float depth01 = texelFetch(_flw_depthPyramid, bounds.xw, level).r; + float depth11 = texelFetch(_flw_depthPyramid, bounds.zw, level).r; + float depth10 = texelFetch(_flw_depthPyramid, bounds.zy, level).r; + float depth00 = texelFetch(_flw_depthPyramid, bounds.xy, level).r; - float depthSphere = 1. + _flw_cullData.znear / (center.z + radius); + float depth; + if (_flw_cullData.useMin == 0) { + depth = max(max(depth00, depth01), max(depth10, depth11)); + } else { + depth = min(min(depth00, depth01), min(depth10, depth11)); + } - isVisible = isVisible && depthSphere <= depth; + float depthSphere = 1. + _flw_cullData.znear / (center.z + radius); + + isVisible = isVisible && depthSphere <= depth; + } } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java index 6d2f0ed66..e8f95baf5 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java @@ -2,7 +2,6 @@ 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 type, InstanceHandle handle) { @@ -49,9 +47,12 @@ public ColoredLitInstance color(byte red, byte green, byte blue) { 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 diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java new file mode 100644 index 000000000..ab0725d9b --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java @@ -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 type, InstanceHandle handle) { + super(type, handle); + } + + public ColoredLitOverlayInstance overlay(int overlay) { + this.overlay = overlay; + return this; + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java new file mode 100644 index 000000000..4278913a7 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/GlyphInstance.java @@ -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 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)); + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java index da093b634..c47cfadcb 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/InstanceTypes.java @@ -103,6 +103,27 @@ public final class InstanceTypes { .cullShader(Flywheel.rl("instance/cull/shadow.glsl")) .build(); + public static final InstanceType 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() { } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java index 321caf936..db59429e2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java @@ -10,7 +10,7 @@ import net.minecraft.core.Vec3i; import net.minecraft.world.phys.Vec3; -public class OrientedInstance extends ColoredLitInstance implements Rotate { +public class OrientedInstance extends ColoredLitOverlayInstance implements Rotate { public float posX; public float posY; public float posZ; diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java index 43e6df47b..f00a6ac5d 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java @@ -13,7 +13,7 @@ import dev.engine_room.flywheel.lib.transform.Transform; import net.minecraft.util.Mth; -public class PosedInstance extends ColoredLitInstance implements Transform { +public class PosedInstance extends ColoredLitOverlayInstance implements Transform { public final Matrix4f pose = new Matrix4f(); public final Matrix3f normal = new Matrix3f(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java index 8bab28975..97ad11337 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java @@ -10,7 +10,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType; import dev.engine_room.flywheel.lib.transform.Affine; -public class TransformedInstance extends ColoredLitInstance implements Affine { +public class TransformedInstance extends ColoredLitOverlayInstance implements Affine { public final Matrix4f pose = new Matrix4f(); public TransformedInstance(InstanceType type, InstanceHandle handle) { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java new file mode 100644 index 000000000..5cfa0e45e --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/BakedGlyphExtension.java @@ -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); +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java index 9a112c07d..0920cf104 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/internal/FlwLibLink.java @@ -10,7 +10,11 @@ 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"); @@ -24,4 +28,10 @@ public interface FlwLibLink { void compileModelPart(ModelPart part, PoseStack.Pose pose, VertexConsumer consumer, int light, int overlay, float red, float green, float blue, float alpha); Deque getPoseStack(PoseStack stack); + + FontSet getFontSet(Font font, ResourceLocation loc); + + boolean getFilterFishyGlyphs(Font font); + + BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph); } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java b/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java index 3e15c76ca..ac9e351cc 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/material/LightShaders.java @@ -3,7 +3,7 @@ import dev.engine_room.flywheel.api.Flywheel; import dev.engine_room.flywheel.api.material.LightShader; -public class LightShaders { +public final class LightShaders { public static final LightShader SMOOTH_WHEN_EMBEDDED = new SimpleLightShader(Flywheel.rl("light/smooth_when_embedded.glsl")); public static final LightShader SMOOTH = new SimpleLightShader(Flywheel.rl("light/smooth.glsl")); public static final LightShader FLAT = new SimpleLightShader(Flywheel.rl("light/flat.glsl")); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java b/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java index cf271ed43..214712af4 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/material/SimpleMaterial.java @@ -178,6 +178,7 @@ public Builder copyFrom(Material material) { shaders = material.shaders(); fog = material.fog(); cutout = material.cutout(); + light = material.light(); texture = material.texture(); blur = material.blur(); mipmap = material.mipmap(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java index 6fa5f33fe..363888f1a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java @@ -33,4 +33,32 @@ public static byte packNormI8(float f) { 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; + } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java index 0e0cb8301..e9032b931 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java @@ -125,7 +125,7 @@ public void delete() { } 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() { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java new file mode 100644 index 000000000..39bdd1dd5 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/SimpleTextLayer.java @@ -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); + } + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java new file mode 100644 index 000000000..35151c5d2 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayer.java @@ -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 + @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 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); + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java new file mode 100644 index 000000000..57701ce76 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextLayers.java @@ -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() { + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java new file mode 100644 index 000000000..cac40b934 --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/text/TextVisual.java @@ -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 GLYPH_MESH_CACHE = new ResourceReloadCache<>(GlyphMeshKey::into); + private static final ResourceReloadCache GLYPH_MODEL_CACHE = new ResourceReloadCache<>(GlyphModelKey::into); + + private static final ThreadLocal SINKS = ThreadLocal.withInitial(Sink::new); + + private final SmartRecycler recycler; + private final List 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 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 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 recycler; + @UnknownNullability + private List layers; + @UnknownNullability + private Matrix4f pose; + private int light; + + private float x; + + public void prepare(SmartRecycler recycler, List 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; + } + } +} diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl new file mode 100644 index 000000000..c8d9a275c --- /dev/null +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/cull/glyph.glsl @@ -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); +} diff --git a/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert new file mode 100644 index 000000000..6592d5e8c --- /dev/null +++ b/common/src/lib/resources/assets/flywheel/flywheel/instance/glyph.vert @@ -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; +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java index 47fffeeb6..c156f5668 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwLibLinkImpl.java @@ -11,9 +11,15 @@ 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 @@ -40,4 +46,19 @@ public void compileModelPart(ModelPart part, PoseStack.Pose pose, VertexConsumer public Deque getPoseStack(PoseStack stack) { return ((PoseStackAccessor) stack).flywheel$getPoseStack(); } + + @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; + } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java b/common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java new file mode 100644 index 000000000..eb1c15ee2 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FontTextureUpload.java @@ -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) { +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java b/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java new file mode 100644 index 000000000..2db5e5462 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/extension/FontTextureExtension.java @@ -0,0 +1,7 @@ +package dev.engine_room.flywheel.impl.extension; + +import net.minecraft.resources.ResourceLocation; + +public interface FontTextureExtension { + void flywheel$setName(ResourceLocation name); +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java new file mode 100644 index 000000000..e18ffb5c5 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/BakedGlyphMixin.java @@ -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; + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java new file mode 100644 index 000000000..5c9f245bc --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/CodePointMapMixin.java @@ -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 { + @Unique + private final Object flywheel$lock = new Object(); + + @WrapMethod(method = "clear") + private void flywheel$wrapClearAsSynchronized(Operation original) { + synchronized (flywheel$lock) { + original.call(); + } + } + + @WrapMethod(method = "get") + private T flywheel$wrapGetAsSynchronized(int index, Operation original) { + synchronized (flywheel$lock) { + return original.call(index); + } + } + + @WrapMethod(method = "put") + private T flywheel$wrapPutAsSynchronized(int index, T value, Operation original) { + synchronized (flywheel$lock) { + return original.call(index, value); + } + } + + @WrapMethod(method = "computeIfAbsent") + private T flywheel$wrapComputeIfAbsentAsSynchronized(int index, IntFunction valueIfAbsentGetter, Operation original) { + synchronized (flywheel$lock) { + return original.call(index, valueIfAbsentGetter); + } + } + + @WrapMethod(method = "remove") + private T flywheel$wrapRemoveAsSynchronized(int index, Operation original) { + synchronized (flywheel$lock) { + return original.call(index); + } + } + + @WrapMethod(method = "forEach") + private void flywheel$wrapForEachAsSynchronized(CodepointMap.Output output, Operation original) { + synchronized (flywheel$lock) { + original.call(output); + } + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java new file mode 100644 index 000000000..bb37a7212 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontAccessor.java @@ -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); +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java new file mode 100644 index 000000000..8970ae216 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontSetMixin.java @@ -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; + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java new file mode 100644 index 000000000..d2723f645 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTexture$NodeAccessor.java @@ -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(); +} diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java new file mode 100644 index 000000000..bd00aac1c --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/impl/mixin/text/FontTextureMixin.java @@ -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 flywheel$uploads = new ArrayList<>(); + @Unique + private boolean flywheel$flushScheduled = false; + + @Unique + private ResourceLocation flywheel$name; + + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;getId()I")) + private int flywheel$skipGetId(FontTexture instance, Operation 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 = "", 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 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 original, @Share("node") LocalRef 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 cir, @Share("node") LocalRef 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; + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java new file mode 100644 index 000000000..c772bb425 --- /dev/null +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/SignVisual.java @@ -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 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 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 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 list = FONT.split(component, blockEntity.getMaxTextLineWidth()); + return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); + }); + + List 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()); + } +} diff --git a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java index 53efc4562..cadda5114 100644 --- a/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java +++ b/common/src/main/java/dev/engine_room/flywheel/vanilla/VanillaVisuals.java @@ -47,6 +47,9 @@ public static void init() { .factory(ShulkerBoxVisual::new) .apply(); + builder(BlockEntityType.SIGN).factory(SignVisual::new) + .apply(); + builder(EntityType.CHEST_MINECART) .factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.CHEST_MINECART)) .skipVanillaRender(MinecartVisual::shouldSkipRender) diff --git a/common/src/main/resources/flywheel.impl.mixins.json b/common/src/main/resources/flywheel.impl.mixins.json index a1953a127..3d4bdb723 100644 --- a/common/src/main/resources/flywheel.impl.mixins.json +++ b/common/src/main/resources/flywheel.impl.mixins.json @@ -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", diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index c9569e26c..84d3ea2f0 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -87,6 +87,9 @@ dependencies { modCompileOnly("maven.modrinth:embeddium:${property("embeddium_version")}") modCompileOnly("maven.modrinth:oculus:${property("oculus_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 = "commonApiOnly")) "forLib"(project(path = ":common", configuration = "commonLib")) "forBackend"(project(path = ":common", configuration = "commonBackend")) diff --git a/gradle.properties b/gradle.properties index d8c4ff106..418aaff91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,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