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 { 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 } 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() 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, 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): 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) } } }