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

354 lines
8.0 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.render
import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import org.lwjgl.opengl.GL46.*
import org.w3c.dom.Text
import ru.dbotthepony.kstarbound.freetype.LoadFlag
import ru.dbotthepony.kstarbound.freetype.struct.FT_Pixel_Mode
import ru.dbotthepony.kstarbound.gl.*
import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.util.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 = "./unpacked_assets/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.translateWithScale(x = x, y = lineHeight * scale + y)
TextAlignY.CENTER -> stack.translateWithScale(x = x, y = lineHeight * scale - totalY * scale / 2f + y)
TextAlignY.BOTTOM -> stack.translateWithScale(x = x, y = lineHeight * scale - totalY * scale + y)
}
if (scale != 1f)
stack.scale(x = scale, y = scale)
state.fontProgram.use()
state.fontProgram.color.set(color)
state.activeTexture = 0
val space = getGlyph(' ')
var advancedX = 0f
for (line in text) {
if (line == "") {
stack.translateWithScale(y = lineHeight)
continue
}
var movedX = 0f
when (alignX) {
TextAlignX.LEFT -> {}
TextAlignX.CENTER -> {
movedX = totalX / 2f - lineWidth(line, space) / 2f
stack.translateWithScale(x = movedX)
}
TextAlignX.RIGHT -> {
movedX = -lineWidth(line, space)
stack.translateWithScale(x = movedX)
}
}
var lineGlyphs = 0
var lineWidth = 0f
for (chr in line) {
if (chr == '\t') {
if (lineGlyphs % 4 == 0) {
advancedX += space.advanceX * 4
stack.translateWithScale(x = space.advanceX * 4)
} else {
advancedX += space.advanceX * (lineGlyphs % 4)
stack.translateWithScale(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.translateWithScale(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.translateWithScale(x = space.advanceX * 4)
} else {
lineWidth += space.advanceX * (lineGlyphs % 4)
state.matrixStack.translateWithScale(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: GLVertexBufferObject?
private val ebo: GLVertexBufferObject?
private val vao: GLVertexArrayObject?
private val indexCount: 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(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS)
builder.quad(0f, 0f, width, height, VertexTransformers.uv())
builder.upload(vbo, ebo, GL_STATIC_DRAW)
builder.attributes.apply(vao, true)
indexCount = builder.indexCount
vao.unbind()
ebo.unbind()
vbo.unbind()
} else {
isEmpty = true
indexCount = 0
vao = null
vbo = null
ebo = null
texture = null
}
}
fun render(stack: Matrix4fStack) {
if (isEmpty) {
stack.translateWithScale(advanceX)
return
}
vao!!.bind()
stack.translateWithScale(bearingX, -bearingY)
texture!!.bind()
state.fontProgram.transform.set(stack.last)
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0L)
checkForGLError()
stack.translateWithScale(advanceX - bearingX, bearingY)
}
override fun close() {
vao?.close()
ebo?.close()
vbo?.close()
texture?.close()
}
}
}