From f4e1db5846c517597538d2c87317239934f6bd4c Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 14 Mar 2026 14:24:23 +0900 Subject: [PATCH] glyph texture atlas --- src/assets/unipunct_variable.tga | 2 +- .../terrarumsansbitmap/gdx/GlyphAtlas.kt | 81 +++++++ .../gdx/TerrarumSansBitmap.kt | 198 ++++++++---------- work_files/cyrilic_variable.psd | 4 +- work_files/maths1_extrawide_variable.kra | 4 +- work_files/unipunct_variable.psd | 4 +- 6 files changed, 176 insertions(+), 117 deletions(-) create mode 100644 src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt diff --git a/src/assets/unipunct_variable.tga b/src/assets/unipunct_variable.tga index 3b8b0dc..314baff 100755 --- a/src/assets/unipunct_variable.tga +++ b/src/assets/unipunct_variable.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:498f0f5267f2aa8f6acbd1dfc1e575812fe243be5456bb24b06a58b8a63a5ab6 +oid sha256:ec8c23a2ced6bea3d9cd7bcc7e2f303d8fc31e45f954663221e87d3a91d7c5b4 size 204818 diff --git a/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt b/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt new file mode 100644 index 0000000..4382d28 --- /dev/null +++ b/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt @@ -0,0 +1,81 @@ +package net.torvald.terrarumsansbitmap.gdx + +import com.badlogic.gdx.graphics.Pixmap + +data class AtlasRegion( + val atlasX: Int, + val atlasY: Int, + val width: Int, + val height: Int, + val offsetX: Int = 0, + val offsetY: Int = 0 +) + +class GlyphAtlas(val atlasWidth: Int, val atlasHeight: Int) { + + val pixmap = Pixmap(atlasWidth, atlasHeight, Pixmap.Format.RGBA8888).also { it.blending = Pixmap.Blending.None } + + private val regions = HashMap() + + private var cursorX = 0 + private var cursorY = 0 + private var shelfHeight = 0 + + private fun atlasKey(sheetID: Int, cellX: Int, cellY: Int): Long = + sheetID.toLong().shl(32) or cellX.toLong().shl(16) or cellY.toLong() + + fun packCell(sheetID: Int, cellX: Int, cellY: Int, cellPixmap: Pixmap) { + val w = cellPixmap.width + val h = cellPixmap.height + + if (cursorX + w > atlasWidth) { + cursorX = 0 + cursorY += shelfHeight + shelfHeight = 0 + } + + pixmap.drawPixmap(cellPixmap, cursorX, cursorY) + + regions[atlasKey(sheetID, cellX, cellY)] = AtlasRegion(cursorX, cursorY, w, h) + + cursorX += w + if (h > shelfHeight) shelfHeight = h + } + + fun blitSheet(sheetID: Int, sheetPixmap: Pixmap, cellW: Int, cellH: Int, cols: Int, rows: Int) { + if (cursorX > 0) { + cursorX = 0 + cursorY += shelfHeight + shelfHeight = 0 + } + + val baseY = cursorY + + pixmap.drawPixmap(sheetPixmap, 0, baseY) + + for (cy in 0 until rows) { + for (cx in 0 until cols) { + regions[atlasKey(sheetID, cx, cy)] = AtlasRegion(cx * cellW, baseY + cy * cellH, cellW, cellH) + } + } + + cursorY = baseY + sheetPixmap.height + cursorX = 0 + shelfHeight = 0 + } + + fun getRegion(sheetID: Int, cellX: Int, cellY: Int): AtlasRegion? = + regions[atlasKey(sheetID, cellX, cellY)] + + fun clearRegion(region: AtlasRegion) { + for (y in 0 until region.height) { + for (x in 0 until region.width) { + pixmap.drawPixel(region.atlasX + x, region.atlasY + y, 0) + } + } + } + + fun dispose() { + pixmap.dispose() + } +} diff --git a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt index fa046a3..e93da5d 100755 --- a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt @@ -318,7 +318,7 @@ class TerrarumSansBitmap( /** Props of all printable Unicode points. */ private val glyphProps = HashMap() private val textReplaces = HashMap() - private val sheets: Array + private lateinit var atlas: GlyphAtlas // private var charsetOverride = 0 @@ -326,9 +326,10 @@ class TerrarumSansBitmap( // private val tempFiles = ArrayList() init { - val sheetsPack = ArrayList() + atlas = GlyphAtlas(4096, 4096) + var unihanPixmap: Pixmap? = null - // first we create pixmap to read pixels, then make texture using pixmap + // first we create pixmap to read pixels, then pack into atlas fileList.forEachIndexed { index, it -> val isVariable = it.endsWith("_variable.tga") val isXYSwapped = it.contains("xyswap", true) @@ -346,32 +347,6 @@ class TerrarumSansBitmap( else dbgprn("loading texture [STATIC] $it") - - // unpack gz if applicable - /*if (it.endsWith(".gz")) { - val tmpFilePath = tempDir + "/tmp_${it.dropLast(7)}.tga" - - try { - val gzi = GZIPInputStream(Gdx.files.classpath(fontParentDir + it).read(8192)) - val wholeFile = gzi.readBytes() - gzi.close() - val fos = BufferedOutputStream(FileOutputStream(tmpFilePath)) - fos.write(wholeFile) - fos.flush() - fos.close() - - pixmap = Pixmap(Gdx.files.absolute(tmpFilePath)) -// tempFiles.add(tmpFilePath) - } - catch (e: GdxRuntimeException) { - //e.printStackTrace() - dbgprn("said texture not found, skipping...") - - pixmap = Pixmap(1, 1, Pixmap.Format.RGBA8888) - } - //File(tmpFileName).delete() - } - else {*/ try { pixmap = Pixmap(Gdx.files.classpath("assets/$it")) } @@ -387,7 +362,6 @@ class TerrarumSansBitmap( System.exit(1) } } - //} if (isVariable) buildWidthTable(pixmap, codeRange[index], if (isExtraWide) 32 else 16) buildWidthTableFixed() @@ -395,40 +369,50 @@ class TerrarumSansBitmap( setupDynamicTextReplacer() - /*if (!noShadow) { - makeShadowForSheet(pixmap) - }*/ + if (index == SHEET_UNIHAN) { + // defer wenquanyi packing to after all other sheets + unihanPixmap = pixmap + } + else { + val texRegPack = if (isExtraWide) + PixmapRegionPack(pixmap, W_WIDEVAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) + else if (isVariable) + PixmapRegionPack(pixmap, W_VAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) + else if (index == SHEET_HANGUL) + PixmapRegionPack(pixmap, W_HANGUL_BASE, H) + else if (index == SHEET_CUSTOM_SYM) + PixmapRegionPack(pixmap, SIZE_CUSTOM_SYM, SIZE_CUSTOM_SYM) + else if (index == SHEET_RUNIC) + PixmapRegionPack(pixmap, W_LATIN_WIDE, H) + else throw IllegalArgumentException("Unknown sheet index: $index") + for (cy in 0 until texRegPack.verticalCount) { + for (cx in 0 until texRegPack.horizontalCount) { + atlas.packCell(index, cx, cy, texRegPack.get(cx, cy)) + } + } - //val texture = Texture(pixmap) - val texRegPack = if (isExtraWide) - PixmapRegionPack(pixmap, W_WIDEVAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) - else if (isVariable) - PixmapRegionPack(pixmap, W_VAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) - else if (index == SHEET_UNIHAN) - PixmapRegionPack(pixmap, W_UNIHAN, H_UNIHAN) // the only exception that is height is 16 - // below they all have height of 20 'H' - else if (index == SHEET_HANGUL) - PixmapRegionPack(pixmap, W_HANGUL_BASE, H) - else if (index == SHEET_CUSTOM_SYM) - PixmapRegionPack(pixmap, SIZE_CUSTOM_SYM, SIZE_CUSTOM_SYM) // TODO variable - else if (index == SHEET_RUNIC) - PixmapRegionPack(pixmap, W_LATIN_WIDE, H) - else throw IllegalArgumentException("Unknown sheet index: $index") - - //texRegPack.texture.setFilter(minFilter, magFilter) - - sheetsPack.add(texRegPack) - - pixmap.dispose() // you are terminated + texRegPack.dispose() + pixmap.dispose() + } } - sheets = sheetsPack.toTypedArray() + // pack wenquanyi (SHEET_UNIHAN) last as a contiguous blit + unihanPixmap?.let { + val cols = it.width / W_UNIHAN + val rows = it.height / H_UNIHAN + atlas.blitSheet(SHEET_UNIHAN, it, W_UNIHAN, H_UNIHAN, cols, rows) + it.dispose() + } // make sure null char is actually null (draws nothing and has zero width) - sheets[SHEET_ASCII_VARW].regions[0].setColor(0) - sheets[SHEET_ASCII_VARW].regions[0].fill() + atlas.getRegion(SHEET_ASCII_VARW, 0, 0)?.let { atlas.clearRegion(it) } glyphProps[0] = GlyphProps(0) + + if (debug) { + com.badlogic.gdx.graphics.PixmapIO.writePNG(Gdx.files.absolute("$tempDir/glyph_atlas_dump.png"), atlas.pixmap) + dbgprn("atlas dumped to $tempDir/glyph_atlas_dump.png") + } } override fun getLineHeight(): Float = LINE_HEIGHT.toFloat() * scale @@ -599,39 +583,31 @@ class TerrarumSansBitmap( val (indexCho, indexJung, indexJong) = indices val (choRow, jungRow, jongRow) = rows - val hangulSheet = sheets[SHEET_HANGUL] - - - - val choTex = hangulSheet.get(indexCho, choRow) - val jungTex = hangulSheet.get(indexJung, jungRow) - val jongTex = hangulSheet.get(indexJong, jongRow) - - linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) - linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) - linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + atlas.getRegion(SHEET_HANGUL, indexCho, choRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } + atlas.getRegion(SHEET_HANGUL, indexJung, jungRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } + atlas.getRegion(SHEET_HANGUL, indexJong, jongRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } index += hangulLength - 1 } else { - try { - val posY = posmap.y[index].flipY() + - if (sheetID == SHEET_UNIHAN) // evil exceptions - offsetUnihan - else if (sheetID == SHEET_CUSTOM_SYM) - offsetCustomSym - else 0 + val posY = posmap.y[index].flipY() + + if (sheetID == SHEET_UNIHAN) // evil exceptions + offsetUnihan + else if (sheetID == SHEET_CUSTOM_SYM) + offsetCustomSym + else 0 - val posX = posmap.x[index] - val texture = sheets[sheetID].get(sheetX, sheetY) - - linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol) - - - } - catch (noSuchGlyph: ArrayIndexOutOfBoundsException) { + val posX = posmap.x[index] + atlas.getRegion(sheetID, sheetX, sheetY)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posX + linotypePaddingX, posY + linotypePaddingY, renderCol) } } @@ -715,39 +691,31 @@ class TerrarumSansBitmap( val (indexCho, indexJung, indexJong) = indices val (choRow, jungRow, jongRow) = rows - val hangulSheet = sheets[SHEET_HANGUL] - - - - val choTex = hangulSheet.get(indexCho, choRow) - val jungTex = hangulSheet.get(indexJung, jungRow) - val jongTex = hangulSheet.get(indexJong, jongRow) - - linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) - linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) - linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + atlas.getRegion(SHEET_HANGUL, indexCho, choRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } + atlas.getRegion(SHEET_HANGUL, indexJung, jungRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } + atlas.getRegion(SHEET_HANGUL, indexJong, jongRow)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol) + } index += hangulLength - 1 } else { - try { - val posY = posmap.y[index].flipY() + - if (sheetID == SHEET_UNIHAN) // evil exceptions - offsetUnihan - else if (sheetID == SHEET_CUSTOM_SYM) - offsetCustomSym - else 0 + val posY = posmap.y[index].flipY() + + if (sheetID == SHEET_UNIHAN) // evil exceptions + offsetUnihan + else if (sheetID == SHEET_CUSTOM_SYM) + offsetCustomSym + else 0 - val posX = posmap.x[index] - val texture = sheets[sheetID].get(sheetX, sheetY) - - linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol) - - - } - catch (noSuchGlyph: ArrayIndexOutOfBoundsException) { + val posX = posmap.x[index] + atlas.getRegion(sheetID, sheetX, sheetY)?.let { + linotypePixmap.drawFromAtlas(atlas.pixmap, it, posX + linotypePaddingX, posY + linotypePaddingY, renderCol) } } @@ -826,7 +794,7 @@ class TerrarumSansBitmap( override fun dispose() { super.dispose() textCache.values.forEach { it.dispose() } - sheets.forEach { it.dispose() } + atlas.dispose() } fun getSheetType(c: CodePoint): Int { @@ -2163,6 +2131,16 @@ class TerrarumSansBitmap( } } + private fun Pixmap.drawFromAtlas(atlas: Pixmap, region: AtlasRegion, xPos: Int, yPos: Int, col: Int) { + for (y in 0 until region.height) { + for (x in 0 until region.width) { + val pixel = atlas.getPixel(region.atlasX + x, region.atlasY + y) + val newPixel = pixel colorTimes col + this.drawPixel(xPos + x + region.offsetX, yPos + y + region.offsetY, newPixel) + } + } + } + private fun Color.toRGBA8888() = (this.r * 255f).toInt().shl(24) or (this.g * 255f).toInt().shl(16) or diff --git a/work_files/cyrilic_variable.psd b/work_files/cyrilic_variable.psd index b50ccad..2329adc 100644 --- a/work_files/cyrilic_variable.psd +++ b/work_files/cyrilic_variable.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8b29c4f77f6f30a84d325481339bdf828fbf2132a5be6fc2e82cd1bc7a7fae5 -size 419540 +oid sha256:ecc52281ecc896027a21c045f6e4b321b55a46688f145257bd64d3f5f9fceb42 +size 445138 diff --git a/work_files/maths1_extrawide_variable.kra b/work_files/maths1_extrawide_variable.kra index 1179493..c75b389 100644 --- a/work_files/maths1_extrawide_variable.kra +++ b/work_files/maths1_extrawide_variable.kra @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1075506a1a8e6b71525aacf8cc5593fcc8ed35c711af14ae081c5ce3d1868cb9 -size 69809 +oid sha256:44046b4dd5773a453285b213b9101fcfd20d47ee116993050299d5df7eff270d +size 74335 diff --git a/work_files/unipunct_variable.psd b/work_files/unipunct_variable.psd index 1892baa..3d2b9a9 100644 --- a/work_files/unipunct_variable.psd +++ b/work_files/unipunct_variable.psd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c5e8d8e6b31e84fc8431f6318e87054e51104ea318d2f8526367613e7ed1982 -size 180790 +oid sha256:3e0975cf931ba05009e6dc67481b7d26ded63871752b91c8e646ce9359eafc62 +size 185967