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 { 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 = "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.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): 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() } } }