KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt

362 lines
8.7 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.Object2ObjectFunction
import org.lwjgl.opengl.GL46.*
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.GLAttributeList
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.vector.Color
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
}
class Font(
val state: GLStateTracker,
val font: String = "hobo.ttf",
val size: Int = 48
) {
val face = state.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
fun render(
text: List<String>,
x: Float = 0f,
y: Float = 0f,
alignX: TextAlignX = TextAlignX.LEFT,
alignY: TextAlignY = TextAlignY.TOP,
color: Color = Color.WHITE,
scale: Float = 1f,
stack: Matrix4fStack = state.matrixStack,
): 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
stack.push()
when (alignY) {
TextAlignY.TOP -> stack.translateWithMultiplication(x = x, y = lineHeight * scale + y)
TextAlignY.CENTER -> stack.translateWithMultiplication(x = x, y = lineHeight * scale - totalY * scale / 2f + y)
TextAlignY.BOTTOM -> stack.translateWithMultiplication(x = x, y = lineHeight * scale - totalY * scale + y)
}
if (scale != 1f)
stack.scale(x = scale, y = scale)
state.programs.font.use()
state.programs.font.color = color
state.activeTexture = 0
val space = getGlyph(' ')
var advancedX = 0f
for (line in text) {
if (line == "") {
stack.translateWithMultiplication(y = lineHeight)
continue
}
var movedX = 0f
when (alignX) {
TextAlignX.LEFT -> {}
TextAlignX.CENTER -> {
movedX = totalX / 2f - lineWidth(line, space) / 2f
stack.translateWithMultiplication(x = movedX)
}
TextAlignX.RIGHT -> {
movedX = -lineWidth(line, space)
stack.translateWithMultiplication(x = movedX)
}
}
var lineGlyphs = 0
var lineWidth = 0f
for (chr in line) {
if (chr == '\t') {
if (lineGlyphs % 4 == 0) {
advancedX += space.advanceX * 4
stack.translateWithMultiplication(x = space.advanceX * 4)
} else {
advancedX += space.advanceX * (lineGlyphs % 4)
stack.translateWithMultiplication(x = space.advanceX * (lineGlyphs % 4))
}
lineGlyphs += lineGlyphs % 4
continue
}
val glyph = getGlyph(chr)
glyph.render(stack)
lineWidth += glyph.advanceX
lineGlyphs++
}
advancedX = advancedX.coerceAtLeast(lineWidth)
stack.translateWithMultiplication(x = -lineWidth - movedX, y = lineHeight)
}
state.VAO = null
stack.pop()
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: Color = Color.WHITE,
scale: Float = 1f,
stack: Matrix4fStack = state.matrixStack,
): TextSize {
return render(
breakLines(text),
x = x,
y = y,
alignX = alignX,
alignY = alignY,
scale = scale,
stack = stack,
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
state.matrixStack.translateWithMultiplication(x = space.advanceX * 4)
} else {
lineWidth += space.advanceX * (lineGlyphs % 4)
state.matrixStack.translateWithMultiplication(x = 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) : AutoCloseable {
private val texture: GLTexture2D?
val isEmpty: Boolean
val width: Float
val height: Float
val bearingX: Float
val bearingY: Float
val advanceX: Float
val advanceY: Float
private val vbo: VertexBufferObject? // все три указателя должны хранится во избежание утечки
private val ebo: VertexBufferObject? // все три указателя должны хранится во избежание утечки
private val vao: VertexArrayObject? // все три указателя должны хранится во избежание утечки
private val indexCount: Int
private val elementIndexType: Int
init {
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!!
texture = state.newTexture("$font:$char")
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
texture.upload(GL_RED, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff)
texture.generateMips()
texture.textureMinFilter = GL_LINEAR
texture.textureMagFilter = GL_LINEAR
texture.textureWrapS = GL_CLAMP_TO_EDGE
texture.textureWrapT = GL_CLAMP_TO_EDGE
glPixelStorei(GL_UNPACK_ALIGNMENT, 4)
vao = state.newVAO()
ebo = state.newEBO()
vbo = state.newVBO()
vao.bind()
ebo.bind()
vbo.bind()
val builder = VertexBuilder(GLAttributeList.VERTEX_2D_TEXTURE, GeometryType.QUADS)
builder.quad(0f, 0f, width, height, QuadTransformers.uv())
builder.upload(vbo, ebo, GL_STATIC_DRAW)
builder.attributes.apply(vao, true)
indexCount = builder.indexCount
elementIndexType = builder.indexType
vao.unbind()
ebo.unbind()
vbo.unbind()
} else {
isEmpty = true
indexCount = 0
elementIndexType = 0
vao = null
vbo = null
ebo = null
texture = null
}
}
fun render(stack: Matrix4fStack) {
if (isEmpty) {
stack.translateWithMultiplication(advanceX)
return
}
vao!!.bind()
stack.translateWithMultiplication(bearingX, -bearingY)
texture!!.bind()
state.programs.font.transform = stack.last
glDrawElements(GL_TRIANGLES, indexCount, elementIndexType, 0L)
checkForGLError()
stack.translateWithMultiplication(advanceX - bearingX, bearingY)
}
override fun close() {
vao?.close()
ebo?.close()
vbo?.close()
texture?.close()
}
}
}