diff --git a/api/Elementa.api b/api/Elementa.api index be070249..02ff7f0b 100644 --- a/api/Elementa.api +++ b/api/Elementa.api @@ -2892,9 +2892,13 @@ public final class gg/essential/elementa/font/data/AtlasBounds { public final fun copy (FFFF)Lgg/essential/elementa/font/data/AtlasBounds; public static synthetic fun copy$default (Lgg/essential/elementa/font/data/AtlasBounds;FFFFILjava/lang/Object;)Lgg/essential/elementa/font/data/AtlasBounds; public fun equals (Ljava/lang/Object;)Z + public final fun getB ()F public final fun getBottom ()F + public final fun getL ()F public final fun getLeft ()F + public final fun getR ()F public final fun getRight ()F + public final fun getT ()F public final fun getTop ()F public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -2964,9 +2968,13 @@ public final class gg/essential/elementa/font/data/PlaneBounds { public final fun copy (FFFF)Lgg/essential/elementa/font/data/PlaneBounds; public static synthetic fun copy$default (Lgg/essential/elementa/font/data/PlaneBounds;FFFFILjava/lang/Object;)Lgg/essential/elementa/font/data/PlaneBounds; public fun equals (Ljava/lang/Object;)Z + public final fun getB ()F public final fun getBottom ()F + public final fun getL ()F public final fun getLeft ()F + public final fun getR ()F public final fun getRight ()F + public final fun getT ()F public final fun getTop ()F public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/src/main/kotlin/gg/essential/elementa/font/BasicFontRenderer.kt b/src/main/kotlin/gg/essential/elementa/font/BasicFontRenderer.kt index 6972dbe1..c28ce176 100644 --- a/src/main/kotlin/gg/essential/elementa/font/BasicFontRenderer.kt +++ b/src/main/kotlin/gg/essential/elementa/font/BasicFontRenderer.kt @@ -5,7 +5,9 @@ import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.ConstraintType import gg.essential.elementa.constraints.resolution.ConstraintVisitor import gg.essential.elementa.font.data.Font +import gg.essential.elementa.font.data.FontInfo import gg.essential.elementa.font.data.Glyph +import gg.essential.elementa.font.data.shrinkGlyphsByHalfAPixel import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack import gg.essential.universal.render.UGpuSampler @@ -15,10 +17,14 @@ import gg.essential.universal.vertex.UBufferBuilder import gg.essential.universal.vertex.UVertexConsumer import java.awt.Color import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt class BasicFontRenderer( - private val regularFont: Font + regularFont: Font ) : FontProvider { + private val regularFontInfo = regularFont.fontInfo.shrinkGlyphsByHalfAPixel() + private val regularFontTexture by lazy { regularFont.getTexture() } /* Required by Elementa but unused for this type of constraint */ override var cachedValue: FontProvider = this @@ -35,14 +41,15 @@ class BasicFontRenderer( } private fun getStringDimensions(string: String, pointSize: Float): Pair { - var width = 0f - var height = 0f + var currentX = 0f + var top = Float.NEGATIVE_INFINITY + var bottom = Float.POSITIVE_INFINITY /* 10 point font is the default used in Elementa. Adjust the point size based on this font's size. */ - val currentPointSize = pointSize / 10 * regularFont.fontInfo.atlas.size + val currentPointSize = pointSize / 10 * regularFontInfo.atlas.size var i = 0 while (i < string.length) { @@ -55,34 +62,34 @@ class BasicFontRenderer( continue } - val glyph = regularFont.fontInfo.glyphs[char.code] - if (glyph?.atlasBounds == null) { + val glyph = regularFontInfo.glyphs[char.code] + if (glyph == null) { i++ continue } + val planeBounds = glyph.planeBounds if (planeBounds != null) { - height = max((planeBounds.top - planeBounds.bottom) * currentPointSize, height) + top = max(top, planeBounds.t) + bottom = min(bottom, planeBounds.b) } - //The last character should not have the whitespace to the right of it - //Added to the width. Instead, we only add the width of the character - val lastCorrection = if (i < string.length - 1) 0 else 1 - - //The texture atlas is used here because in the context of this implementation of the font renderer - //we do not need or want the full precision the msdf font renderer exports in. Instead, we care about - //calculating width based on the texture pixels - width += (((glyph.atlasBounds.right - glyph.atlasBounds.left - lastCorrection) / regularFont.fontInfo.atlas.size) * currentPointSize) - + currentX += computeAdvance(regularFontInfo, glyph) i++ } - return Pair(width, height) + + // undo letter spacing after final letter + currentX -= 1 / regularFontInfo.atlas.size + + val width = currentX.coerceAtLeast(0f) + val height = if (top.isInfinite() || bottom.isInfinite()) 0f else top - bottom + return Pair(width * currentPointSize, height * currentPointSize) } fun getLineHeight(pointSize: Float): Float { - return regularFont.fontInfo.metrics.lineHeight * pointSize + return regularFontInfo.metrics.lineHeight * pointSize } override fun drawString( @@ -100,7 +107,7 @@ class BasicFontRenderer( val bufferBuilder = UBufferBuilder.create(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) drawString(bufferBuilder, matrixStack, string, color, x, y, originalPointSize / 10 * scale, shadow, shadowColor) bufferBuilder.build()?.drawAndClose(if (ElementaVersion.atLeastV10Active) PIPELINE2 else PIPELINE) { - texture(0, regularFont.getTexture().gpuTextureView, UGpuSampler( + texture(0, regularFontTexture.gpuTextureView, UGpuSampler( UGpuSampler.AddressMode.CLAMP_TO_EDGE, UGpuSampler.AddressMode.CLAMP_TO_EDGE, UGpuSampler.FilterMode.NEAREST, @@ -109,7 +116,7 @@ class BasicFontRenderer( )) } } else { - UGraphics.bindTexture(0, regularFont.getTexture().dynamicGlId) + UGraphics.bindTexture(0, regularFontTexture.dynamicGlId) val bufferBuilder = UGraphics.getFromTessellator() @Suppress("DEPRECATION") bufferBuilder.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) @@ -129,14 +136,8 @@ class BasicFontRenderer( shadow: Boolean, shadowColor: Color? ) { - val scaledPointSize = scale * regularFont.fontInfo.atlas.size + val scaledPointSize = scale * regularFontInfo.atlas.size - /* - Moved one pixel up so that the main body of the text is in - the top left of the component. This change keeps text location - in the same location as the vanilla font renderer relative to - a UIText component. - */ if (shadow) { drawStringNow( vertexConsumer, @@ -146,7 +147,7 @@ class BasicFontRenderer( ((color.rgb and 16579836).shr(2)).or((color.rgb).and(-16777216)) ), x + 1, - y, + y + 1, scaledPointSize, ) } @@ -156,21 +157,21 @@ class BasicFontRenderer( string, color, x, - y - 1, + y, scaledPointSize, ) } override fun getBaseLineHeight(): Float { - return regularFont.fontInfo.atlas.baseCharHeight + return regularFontInfo.atlas.baseCharHeight } override fun getShadowHeight(): Float { - return regularFont.fontInfo.atlas.shadowHeight + return regularFontInfo.atlas.shadowHeight } override fun getBelowLineHeight(): Float { - return regularFont.fontInfo.atlas.belowLineHeight + return regularFontInfo.atlas.belowLineHeight } private fun drawStringNow( @@ -194,7 +195,7 @@ class BasicFontRenderer( } - val glyph = regularFont.fontInfo.glyphs[char.code] + val glyph = regularFontInfo.glyphs[char.code] if (glyph == null) { i++ continue @@ -203,8 +204,8 @@ class BasicFontRenderer( val planeBounds = glyph.planeBounds if (planeBounds != null) { - val width = (planeBounds.right - planeBounds.left) * originalPointSize - val height = (planeBounds.top - planeBounds.bottom) * originalPointSize + val width = (planeBounds.r - planeBounds.l) * originalPointSize + val height = (planeBounds.t - planeBounds.b) * originalPointSize drawGlyph( vertexConsumer, @@ -212,25 +213,25 @@ class BasicFontRenderer( glyph, color, currentX, - y + planeBounds.bottom * originalPointSize, + y + regularFontInfo.atlas.baseCharHeight - planeBounds.t * originalPointSize, width, height ) } - //The texture atlas is used here because in the context of this implementation of the font renderer - //we do not need or want the full precision the msdf font renderer exports in. Instead, we care about - //calculating width based on the texture pixels - if (glyph.atlasBounds != null) { - currentX += (((glyph.atlasBounds.right - glyph.atlasBounds.left) / regularFont.fontInfo.atlas.size) * originalPointSize) - } else { - currentX += (glyph.advance) * originalPointSize - } + currentX += computeAdvance(regularFontInfo, glyph) * originalPointSize i++ } } + // Letter spacing for many fonts is like 1.25px, so we ignore font-provided advance values, and instead derive + // ones directly based on the actual size of the glyph. + private fun computeAdvance(fontInfo: FontInfo, glyph: Glyph): Float = + if (glyph.atlasBounds != null) (glyph.atlasBounds.r - glyph.atlasBounds.l + 1) / fontInfo.atlas.size + // For empty glyphs (like ` `), we use the font-provided value but round it to pixels so we don't end up with + // sub-pixel positions. We don't use `roundToRealPixels`, so the value stays scale-independent. + else (glyph.advance * fontInfo.atlas.size).roundToInt() / fontInfo.atlas.size private fun drawGlyph( worldRenderer: UVertexConsumer, @@ -243,11 +244,11 @@ class BasicFontRenderer( height: Float ) { val atlasBounds = glyph.atlasBounds ?: return - val atlas = regularFont.fontInfo.atlas - val textureTop = 1.0 - ((atlasBounds.top) / atlas.height) - val textureBottom = 1.0 - ((atlasBounds.bottom) / atlas.height) - val textureLeft = (atlasBounds.left / atlas.width).toDouble() - val textureRight = (atlasBounds.right / atlas.width).toDouble() + val atlas = regularFontInfo.atlas + val textureTop = 1.0 - ((atlasBounds.t) / atlas.height) + val textureBottom = 1.0 - ((atlasBounds.b) / atlas.height) + val textureLeft = (atlasBounds.l / atlas.width).toDouble() + val textureRight = (atlasBounds.r / atlas.width).toDouble() val doubleX = x.toDouble() val doubleY = y.toDouble() @@ -290,4 +291,4 @@ class BasicFontRenderer( blendState = BlendState.ALPHA }.build() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/gg/essential/elementa/font/data/FontInfo.kt b/src/main/kotlin/gg/essential/elementa/font/data/FontInfo.kt index aecc9f9d..0ec24536 100644 --- a/src/main/kotlin/gg/essential/elementa/font/data/FontInfo.kt +++ b/src/main/kotlin/gg/essential/elementa/font/data/FontInfo.kt @@ -62,16 +62,21 @@ data class PlaneBounds( @SerializedName("top") private val _top: Float ) { - /** - * msdfgen exports the plane locations with .025 subtracted from the - * Y coordinate of each glyph, so we must correct for this - */ + val l: Float get() = _left + val b: Float get() = _bottom + val r: Float get() = _right + val t: Float get() = _top + + @Deprecated("Returns incorrect value") // technically this one's right, but `top` and `bottom` aren't val left: Float get() = _left + @Deprecated("Returns incorrect value") val bottom: Float get() = _bottom + 0.025f + @Deprecated("Returns incorrect value") // technically this one's right, but `top` and `bottom` aren't val right: Float get() = _right + @Deprecated("Returns incorrect value") val top: Float get() = _top + 0.025f } @@ -87,17 +92,47 @@ data class AtlasBounds( @SerializedName("top") private val _top: Float ) { - /** - * msdfgen exports UV locations in the middle of pixels. - * This causes the rendering to occur slightly of from - * where you would expect it and incorrect texel mapping. - */ + val l: Float get() = _left + val b: Float get() = _bottom + val r: Float get() = _right + val t: Float get() = _top + + @Deprecated("Returns incorrect value") val left: Float get() = _left + .5f + @Deprecated("Returns incorrect value") val bottom: Float get() = _bottom + .5f + @Deprecated("Returns incorrect value") val right: Float get() = _right + .5f + @Deprecated("Returns incorrect value") val top: Float get() = _top + .5f } + +/** + * msdfgen inflates all glyphs by half a pixel so all the edges are drawn properly when using MSDF. + * This function un-does that, because we don't need it in our pixel-font renderer + * ([gg.essential.elementa.font.BasicFontRenderer]) and it makes any sizing math more difficult. + */ +internal fun FontInfo.shrinkGlyphsByHalfAPixel(): FontInfo { + val a = 0.5f // half a pixel in atlas coordinates + val p = a / atlas.size // half a pixel in plane coordinates + return FontInfo( + atlas, + metrics, + glyphs.mapValues { (_, glyph) -> + Glyph( + glyph.unicode, + glyph.advance, + glyph.planeBounds?.let { bounds -> + PlaneBounds(bounds.l + p, bounds.b + p, bounds.r - p, bounds.t - p) + }, + glyph.atlasBounds?.let { bounds -> + AtlasBounds(bounds.l + a, bounds.b + a, bounds.r - a, bounds.t - a) + }, + ) + }, + ) +} diff --git a/src/main/resources/fonts/Minecraft-Five.json b/src/main/resources/fonts/Minecraft-Five.json index a01635ed..d05425ca 100644 --- a/src/main/resources/fonts/Minecraft-Five.json +++ b/src/main/resources/fonts/Minecraft-Five.json @@ -5,7 +5,7 @@ "width": 64, "height": 64, "yOrigin": "bottom", - "baseCharHeight": 5.0, + "baseCharHeight": 6.0, "belowLineHeight": 1.0, "shadowHeight": 1.0 }, @@ -942,9 +942,9 @@ "advance": 0.80000000000000004, "planeBounds": { "left": 0.025000000000000029, - "bottom": 0.475, + "bottom": -0.19500000000000001, "right": 0.77500000000000002, - "top": 0.72499999999999998 + "top": 0.055 }, "atlasBounds": { "left": 40.5,