From 662dc5b093aab1c3b32186599146f525ee6af9f3 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 14 Mar 2026 16:06:31 +0900 Subject: [PATCH] glyph texture atlas (2) --- .../terrarumsansbitmap/gdx/GlyphAtlas.kt | 77 ++++++++++++++++--- .../gdx/TerrarumSansBitmap.kt | 28 ++++++- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt b/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt index 4382d28..0f7acca 100644 --- a/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/GlyphAtlas.kt @@ -13,7 +13,7 @@ data class AtlasRegion( class GlyphAtlas(val atlasWidth: Int, val atlasHeight: Int) { - val pixmap = Pixmap(atlasWidth, atlasHeight, Pixmap.Format.RGBA8888).also { it.blending = Pixmap.Blending.None } + val pixmap = Pixmap(atlasWidth, atlasHeight, Pixmap.Format.RGBA8888).also { it.blending = Pixmap.Blending.SourceOver } private val regions = HashMap() @@ -21,25 +21,78 @@ class GlyphAtlas(val atlasWidth: Int, val atlasHeight: Int) { private var cursorY = 0 private var shelfHeight = 0 + private val pendingCells = ArrayList() + + private class PendingCell( + val sheetID: Int, + val cellX: Int, + val cellY: Int, + val cropped: Pixmap, + val offsetX: Int, + val offsetY: Int + ) + 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 + /** Scans the cell for its non-transparent bounding box, crops, and queues for deferred packing. */ + fun queueCell(sheetID: Int, cellX: Int, cellY: Int, cellPixmap: Pixmap) { + var minX = cellPixmap.width + var minY = cellPixmap.height + var maxX = -1 + var maxY = -1 - if (cursorX + w > atlasWidth) { - cursorX = 0 - cursorY += shelfHeight - shelfHeight = 0 + for (y in 0 until cellPixmap.height) { + for (x in 0 until cellPixmap.width) { + if (cellPixmap.getPixel(x, y) and 0xFF != 0) { + if (x < minX) minX = x + if (y < minY) minY = y + if (x > maxX) maxX = x + if (y > maxY) maxY = y + } + } } - pixmap.drawPixmap(cellPixmap, cursorX, cursorY) + if (maxX < 0) return // entirely transparent, skip - regions[atlasKey(sheetID, cellX, cellY)] = AtlasRegion(cursorX, cursorY, w, h) + val cropW = maxX - minX + 1 + val cropH = maxY - minY + 1 + val cropped = Pixmap(cropW, cropH, Pixmap.Format.RGBA8888) + cropped.drawPixmap(cellPixmap, 0, 0, minX, minY, cropW, cropH) - cursorX += w - if (h > shelfHeight) shelfHeight = h + pendingCells.add(PendingCell(sheetID, cellX, cellY, cropped, minX, minY)) + } + + /** Sorts queued cells by height desc then width desc, and packs into shelves. */ + fun packAllQueued() { + pendingCells.sortWith( + compareByDescending { it.cropped.height } + .thenByDescending { it.cropped.width } + ) + + for (cell in pendingCells) { + val w = cell.cropped.width + val h = cell.cropped.height + + // start new shelf if cell doesn't fit horizontally + if (cursorX + w > atlasWidth) { + cursorX = 0 + cursorY += shelfHeight + shelfHeight = 0 + } + + pixmap.drawPixmap(cell.cropped, cursorX, cursorY) + + regions[atlasKey(cell.sheetID, cell.cellX, cell.cellY)] = + AtlasRegion(cursorX, cursorY, w, h, cell.offsetX, cell.offsetY) + + cursorX += w + if (h > shelfHeight) shelfHeight = h + + cell.cropped.dispose() + } + + pendingCells.clear() } fun blitSheet(sheetID: Int, sheetPixmap: Pixmap, cellW: Int, cellH: Int, cols: Int, rows: Int) { diff --git a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt index e93da5d..1f73161 100755 --- a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt @@ -386,9 +386,19 @@ class TerrarumSansBitmap( PixmapRegionPack(pixmap, W_LATIN_WIDE, H) else throw IllegalArgumentException("Unknown sheet index: $index") + // this code causes initial deva chars to be skipped from rendering +// val illegalCells = HashSet() +// for (code in codeRange[index]) { +// if (glyphProps[code]?.isIllegal == true) { +// val pos = getSheetwisePosition(0, code) +// illegalCells.add(pos[0].toLong().shl(16) or pos[1].toLong()) +// } +// } +// for (cy in 0 until texRegPack.verticalCount) { for (cx in 0 until texRegPack.horizontalCount) { - atlas.packCell(index, cx, cy, texRegPack.get(cx, cy)) +// if (cx.toLong().shl(16) or cy.toLong() in illegalCells) continue + atlas.queueCell(index, cx, cy, texRegPack.get(cx, cy)) } } @@ -397,6 +407,9 @@ class TerrarumSansBitmap( } } + // sort and pack all queued cells (tight-cropped, sorted by height then width) + atlas.packAllQueued() + // pack wenquanyi (SHEET_UNIHAN) last as a contiguous blit unihanPixmap?.let { val cols = it.width / W_UNIHAN @@ -566,6 +579,9 @@ class TerrarumSansBitmap( renderCol = getColour(c) } } + else if (isNoDrawChar(c) || glyphProps[c]?.isIllegal == true) { + // whitespace/control/internal/invalid — no visible glyph, just advance position + } else if (sheetID == SHEET_HANGUL) { // Flookahead for {I, P, F} @@ -674,6 +690,9 @@ class TerrarumSansBitmap( renderCol = getColour(c) } } + else if (isNoDrawChar(c) || glyphProps[c]?.isIllegal == true) { + // whitespace/control/internal/invalid — no visible glyph, just advance position + } else if (sheetID == SHEET_HANGUL) { // Flookahead for {I, P, F} @@ -3008,6 +3027,13 @@ class TerrarumSansBitmap( private fun isBulgarian(c: CodePoint) = c in 0xF0000..0xF005F private fun isSerbian(c: CodePoint) = c in 0xF0060..0xF00BF fun isColourCode(c: CodePoint) = c == 0x100000 || c in 0x10F000..0x10FFFF + private fun isNoDrawChar(c: CodePoint): Boolean = + c <= 0x20 || c == NBSP || c == SHY || c == OBJ || + c in 0x2000..0x200D || + c in 0xD800..0xDFFF || + c in 0xF800..0xF8FF || + c in 0xFFF70..0xFFF9F || + c >= 0xFFFA0 private fun isCharsetOverride(c: CodePoint) = c in 0xFFFC0..0xFFFCF private fun isDevanagari(c: CodePoint) = c in codeRange[SHEET_DEVANAGARI_VARW] private fun isHangulCompat(c: CodePoint) = c in codeRangeHangulCompat