372 lines
9.5 KiB
Kotlin
372 lines
9.5 KiB
Kotlin
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<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
|
||
}
|
||
|
||
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<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
|
||
|
||
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<String>,
|
||
|
||
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<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) {
|
||
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)
|
||
}
|
||
}
|
||
}
|