KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
2024-02-03 20:41:51 +07:00

372 lines
9.5 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.client.render
import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.lwjgl.opengl.GL45.*
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector4f
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.freetype.LoadFlag
import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.client.freetype.struct.FT_Pixel_Mode
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributeType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.defs.image.IUVCoordinates
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
private fun breakLines(text: String): List<String> {
var nextLineBreak = text.indexOf('\n', 0)
if (nextLineBreak == -1) {
return listOf(text)
}
val list = ArrayList<String>()
var previousLineBreak = 0
while (nextLineBreak != -1) {
if (nextLineBreak == previousLineBreak) {
list.add("")
} else {
list.add(text.substring(previousLineBreak, nextLineBreak))
}
if (text.length - 1 <= nextLineBreak) {
break
}
previousLineBreak = nextLineBreak + 1
nextLineBreak = text.indexOf('\n', previousLineBreak)
}
if (previousLineBreak <= text.length - 1) {
list.add(text.substring(previousLineBreak))
}
return list
}
data class TextSize(val width: Float, val height: Float)
enum class TextAlignX {
LEFT, CENTER, RIGHT
}
enum class TextAlignY {
TOP, CENTER, BOTTOM
}
val FONT_VERTEX_FORMAT = VertexAttributes.of(VertexAttributeType.POSITION, VertexAttributeType.UV)
class Font(
val font: String = "hobo.ttf",
val size: Int = 48
) {
val client = StarboundClient.current()
val face = client.freeType.Face(font, 0L)
init {
face.setSize(0, size)
}
private val charMap = Char2ObjectArrayMap<Glyph>()
private fun getGlyph(char: Char): Glyph {
return charMap.computeIfAbsent(char, Object2ObjectFunction {
return@Object2ObjectFunction Glyph(char)
})
}
private val ascender = face.nativeMemory.ascender.toFloat() / ((size / 48f) * 32f)
private val descender = face.nativeMemory.descender.toFloat() / ((size / 48f) * 32f)
val lineHeight = ascender - descender
val bbox = AABBi(
Vector2i(roundTowardsNegativeInfinity(face.nativeMemory.bbox.xMin.toInt() / (12.0 * 48.0 / size)), roundTowardsNegativeInfinity(face.nativeMemory.bbox.yMin.toInt() / (12.0 * 48.0 / size))),
Vector2i(roundTowardsPositiveInfinity(face.nativeMemory.bbox.xMax.toInt() / (12.0 * 48.0 / size)), roundTowardsPositiveInfinity(face.nativeMemory.bbox.yMax.toInt() / (12.0 * 48.0 / size))),
)
private val atlasWidth: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_WIDTH).coerceAtMost(4096)
private val atlasHeight: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_HEIGHT).coerceAtMost(4096)
private var nextAtlasX = 0
private var nextAtlasY = 0
private val atlas = GLTexture2D(atlasWidth, atlasHeight, GL_R8)
init {
atlas.textureMinFilter = GL_LINEAR
atlas.textureMagFilter = GL_LINEAR
atlas.textureWrapS = GL_CLAMP_TO_EDGE
atlas.textureWrapT = GL_CLAMP_TO_EDGE
}
private val builder = VertexBuilder(FONT_VERTEX_FORMAT, GeometryType.QUADS)
private val vao = VertexArrayObject()
private val vbo = BufferObject.VBO()
private val ebo = BufferObject.EBO()
init {
vao.elementBuffer = ebo
vao.bindAttributes(vbo, FONT_VERTEX_FORMAT)
}
fun render(
text: List<String>,
x: Float = 0f,
y: Float = 0f,
alignX: TextAlignX = TextAlignX.LEFT,
alignY: TextAlignY = TextAlignY.TOP,
color: RGBAColor = RGBAColor.WHITE,
scale: Float = 1f,
): TextSize {
if (text.isEmpty())
return TextSize(0f, 0f)
else if (text.size == 1 && text[0] == "")
return TextSize(0f, lineHeight * scale)
val totalSize = size(text)
val totalX = totalSize.width
val totalY = totalSize.height
val model = Matrix3f.identity()
when (alignY) {
TextAlignY.TOP -> model.translate(x = x, y = lineHeight * scale + y)
TextAlignY.CENTER -> model.translate(x = x, y = lineHeight * scale - totalY * scale / 2f + y)
TextAlignY.BOTTOM -> model.translate(x = x, y = lineHeight * scale - totalY * scale + y)
}
if (scale != 1f)
model.scale(x = scale, y = scale)
builder.begin()
val space = getGlyph(' ')
var advancedX = 0f
for (line in text) {
if (line == "") {
model.translate(y = lineHeight)
continue
}
var movedX = 0f
when (alignX) {
TextAlignX.LEFT -> {}
TextAlignX.CENTER -> {
movedX = totalX / 2f - lineWidth(line, space) / 2f
model.translate(x = movedX)
}
TextAlignX.RIGHT -> {
movedX = -lineWidth(line, space)
model.translate(x = movedX)
}
}
var lineGlyphs = 0
var lineWidth = 0f
for (chr in line) {
if (chr == '\t') {
if (lineGlyphs % 4 == 0) {
advancedX += space.advanceX * 4
model.translate(x = space.advanceX * 4)
} else {
advancedX += space.advanceX * (lineGlyphs % 4)
model.translate(x = space.advanceX * (lineGlyphs % 4))
}
lineGlyphs += lineGlyphs % 4
continue
}
val glyph = getGlyph(chr)
glyph.render(model)
lineWidth += glyph.advanceX
lineGlyphs++
}
advancedX = advancedX.coerceAtLeast(lineWidth)
model.translate(x = -lineWidth - movedX, y = lineHeight)
}
builder.end()
if (builder.elementCount != 0) {
client.programs.font.use()
client.programs.font.colorMultiplier = color
client.programs.font.modelMatrix = client.stack.last()
builder.upload(vbo, ebo, GL_STREAM_DRAW)
client.textures2D[0] = atlas
client.vao = vao
glDrawElements(GL_TRIANGLES, builder.indexCount, builder.indexType, 0L)
checkForGLError()
client.vao = null
}
return TextSize(totalX * scale, totalY * scale)
}
fun render(
text: String,
x: Float = 0f,
y: Float = 0f,
alignX: TextAlignX = TextAlignX.LEFT,
alignY: TextAlignY = TextAlignY.TOP,
color: RGBAColor = RGBAColor.WHITE,
scale: Float = 1f,
): TextSize {
return render(
breakLines(text),
x = x,
y = y,
alignX = alignX,
alignY = alignY,
scale = scale,
color = color,
)
}
private fun lineWidth(line: String, space: Glyph): Float {
var lineWidth = 0f
var lineGlyphs = 0
for (chr in line) {
if (chr == '\t') {
if (lineGlyphs % 4 == 0) {
lineWidth += space.advanceX * 4
} else {
lineWidth += space.advanceX * (lineGlyphs % 4)
}
lineGlyphs += lineGlyphs % 4
continue
}
lineWidth += getGlyph(chr).advanceX
lineGlyphs++
}
return lineWidth
}
fun size(lines: List<String>): TextSize {
var advancedX = 0f
var advancedY = 0f
val space = getGlyph(' ')
for (line in lines) {
advancedX = advancedX.coerceAtLeast(lineWidth(line, space))
advancedY += lineHeight
}
return TextSize(advancedX, advancedY)
}
fun size(text: String): TextSize {
return size(breakLines(text))
}
private inner class Glyph(val char: Char) {
val isEmpty: Boolean
val width: Float
val height: Float
val bearingX: Float
val bearingY: Float
val advanceX: Float
val advanceY: Float
val uv: Vector4f
init {
if (nextAtlasY + bbox.height > atlasHeight) {
throw IllegalStateException("Font atlas is full")
}
face.loadChar(char, LoadFlag.RENDER)
val glyph = face.nativeMemory.glyph ?: throw IllegalStateException("Unable to load glyph data for $char (code ${char.code})")
val bitmap = glyph.bitmap
check(bitmap.pixelMode() == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY) { "Unexpected pixel mode of loaded glyph: ${face.nativeMemory.glyph?.bitmap?.pixelMode()} for $char (code ${char.code})" }
width = bitmap.width.toFloat()
height = bitmap.rows.toFloat()
bearingX = glyph.bitmap_left.toFloat()
bearingY = glyph.bitmap_top.toFloat()
advanceX = glyph.advance.x.toFloat() / 64f
advanceY = glyph.advance.y.toFloat() / 64f
if (bitmap.buffer != null) {
isEmpty = false
// интересно, а что будет когда буфер будет "вычищен" из памяти? Чо, будет double free?
val buff = bitmap.byteBuffer!!
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
try {
uv = Vector4f(nextAtlasX / atlasWidth.toFloat(), nextAtlasY / atlasHeight.toFloat(), (nextAtlasX + bitmap.width) / atlasWidth.toFloat(), (nextAtlasY + bitmap.rows) / atlasHeight.toFloat())
atlas.upload(0, nextAtlasX, nextAtlasY, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff)
nextAtlasX += bbox.width
if (nextAtlasX + bbox.width > atlasWidth) {
nextAtlasX = 0
nextAtlasY += bbox.height
}
} finally {
glPixelStorei(GL_UNPACK_ALIGNMENT, 4)
}
} else {
isEmpty = true
uv = Vector4f.ZERO
}
}
fun render(model: Matrix3f) {
if (isEmpty) {
model.translate(advanceX)
return
}
model.translate(bearingX, -bearingY)
builder.vertex(model, 0f, 0f).uv(uv.x, uv.y)
builder.vertex(model, width, 0f).uv(uv.z, uv.y)
builder.vertex(model, width, height).uv(uv.z, uv.w)
builder.vertex(model, 0f, height).uv(uv.x, uv.w)
model.translate(advanceX - bearingX, bearingY)
}
}
}