Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/Elementa.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
101 changes: 51 additions & 50 deletions src/main/kotlin/gg/essential/elementa/font/BasicFontRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -35,14 +41,15 @@ class BasicFontRenderer(
}

private fun getStringDimensions(string: String, pointSize: Float): Pair<Float, Float> {
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) {
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -146,7 +147,7 @@ class BasicFontRenderer(
((color.rgb and 16579836).shr(2)).or((color.rgb).and(-16777216))
),
x + 1,
y,
y + 1,
scaledPointSize,
)
}
Expand All @@ -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(
Expand All @@ -194,7 +195,7 @@ class BasicFontRenderer(
}


val glyph = regularFont.fontInfo.glyphs[char.code]
val glyph = regularFontInfo.glyphs[char.code]
if (glyph == null) {
i++
continue
Expand All @@ -203,34 +204,34 @@ 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,
matrixStack,
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,
Expand All @@ -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()
Expand Down Expand Up @@ -290,4 +291,4 @@ class BasicFontRenderer(
blendState = BlendState.ALPHA
}.build()
}
}
}
53 changes: 44 additions & 9 deletions src/main/kotlin/gg/essential/elementa/font/data/FontInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
},
)
},
)
}
6 changes: 3 additions & 3 deletions src/main/resources/fonts/Minecraft-Five.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"width": 64,
"height": 64,
"yOrigin": "bottom",
"baseCharHeight": 5.0,
"baseCharHeight": 6.0,
"belowLineHeight": 1.0,
"shadowHeight": 1.0
},
Expand Down Expand Up @@ -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,
Expand Down
Loading