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 { var nextLineBreak = text.indexOf('\n', 0) if (nextLineBreak == -1) { return listOf(text) } val list = ArrayList() 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() 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, 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): 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() } } }