362 lines
8.7 KiB
Kotlin
362 lines
8.7 KiB
Kotlin
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()
|
||
}
|
||
}
|
||
}
|