glyph texture atlas

This commit is contained in:
minjaesong
2026-03-14 14:24:23 +09:00
parent 1c7471ccf3
commit f4e1db5846
6 changed files with 176 additions and 117 deletions

Binary file not shown.

View File

@@ -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<Long, AtlasRegion>()
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()
}
}

View File

@@ -318,7 +318,7 @@ class TerrarumSansBitmap(
/** Props of all printable Unicode points. */ /** Props of all printable Unicode points. */
private val glyphProps = HashMap<CodePoint, GlyphProps>() private val glyphProps = HashMap<CodePoint, GlyphProps>()
private val textReplaces = HashMap<CodePoint, CodePoint>() private val textReplaces = HashMap<CodePoint, CodePoint>()
private val sheets: Array<PixmapRegionPack> private lateinit var atlas: GlyphAtlas
// private var charsetOverride = 0 // private var charsetOverride = 0
@@ -326,9 +326,10 @@ class TerrarumSansBitmap(
// private val tempFiles = ArrayList<String>() // private val tempFiles = ArrayList<String>()
init { init {
val sheetsPack = ArrayList<PixmapRegionPack>() 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 -> fileList.forEachIndexed { index, it ->
val isVariable = it.endsWith("_variable.tga") val isVariable = it.endsWith("_variable.tga")
val isXYSwapped = it.contains("xyswap", true) val isXYSwapped = it.contains("xyswap", true)
@@ -346,32 +347,6 @@ class TerrarumSansBitmap(
else else
dbgprn("loading texture [STATIC] $it") 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 { try {
pixmap = Pixmap(Gdx.files.classpath("assets/$it")) pixmap = Pixmap(Gdx.files.classpath("assets/$it"))
} }
@@ -387,7 +362,6 @@ class TerrarumSansBitmap(
System.exit(1) System.exit(1)
} }
} }
//}
if (isVariable) buildWidthTable(pixmap, codeRange[index], if (isExtraWide) 32 else 16) if (isVariable) buildWidthTable(pixmap, codeRange[index], if (isExtraWide) 32 else 16)
buildWidthTableFixed() buildWidthTableFixed()
@@ -395,40 +369,50 @@ class TerrarumSansBitmap(
setupDynamicTextReplacer() setupDynamicTextReplacer()
/*if (!noShadow) { if (index == SHEET_UNIHAN) {
makeShadowForSheet(pixmap) // defer wenquanyi packing to after all other sheets
}*/ unihanPixmap = pixmap
}
else {
//val texture = Texture(pixmap)
val texRegPack = if (isExtraWide) val texRegPack = if (isExtraWide)
PixmapRegionPack(pixmap, W_WIDEVAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) PixmapRegionPack(pixmap, W_WIDEVAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped)
else if (isVariable) else if (isVariable)
PixmapRegionPack(pixmap, W_VAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped) 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) else if (index == SHEET_HANGUL)
PixmapRegionPack(pixmap, W_HANGUL_BASE, H) PixmapRegionPack(pixmap, W_HANGUL_BASE, H)
else if (index == SHEET_CUSTOM_SYM) else if (index == SHEET_CUSTOM_SYM)
PixmapRegionPack(pixmap, SIZE_CUSTOM_SYM, SIZE_CUSTOM_SYM) // TODO variable PixmapRegionPack(pixmap, SIZE_CUSTOM_SYM, SIZE_CUSTOM_SYM)
else if (index == SHEET_RUNIC) else if (index == SHEET_RUNIC)
PixmapRegionPack(pixmap, W_LATIN_WIDE, H) PixmapRegionPack(pixmap, W_LATIN_WIDE, H)
else throw IllegalArgumentException("Unknown sheet index: $index") else throw IllegalArgumentException("Unknown sheet index: $index")
//texRegPack.texture.setFilter(minFilter, magFilter) for (cy in 0 until texRegPack.verticalCount) {
for (cx in 0 until texRegPack.horizontalCount) {
sheetsPack.add(texRegPack) atlas.packCell(index, cx, cy, texRegPack.get(cx, cy))
}
pixmap.dispose() // you are terminated
} }
sheets = sheetsPack.toTypedArray() texRegPack.dispose()
pixmap.dispose()
}
}
// 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) // make sure null char is actually null (draws nothing and has zero width)
sheets[SHEET_ASCII_VARW].regions[0].setColor(0) atlas.getRegion(SHEET_ASCII_VARW, 0, 0)?.let { atlas.clearRegion(it) }
sheets[SHEET_ASCII_VARW].regions[0].fill()
glyphProps[0] = GlyphProps(0) 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 override fun getLineHeight(): Float = LINE_HEIGHT.toFloat() * scale
@@ -599,24 +583,21 @@ class TerrarumSansBitmap(
val (indexCho, indexJung, indexJong) = indices val (indexCho, indexJung, indexJong) = indices
val (choRow, jungRow, jongRow) = rows val (choRow, jungRow, jongRow) = rows
val hangulSheet = sheets[SHEET_HANGUL] 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 {
val choTex = hangulSheet.get(indexCho, choRow) linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
val jungTex = hangulSheet.get(indexJung, jungRow) }
val jongTex = hangulSheet.get(indexJong, jongRow) atlas.getRegion(SHEET_HANGUL, indexJong, jongRow)?.let {
linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
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)
index += hangulLength - 1 index += hangulLength - 1
} }
else { else {
try {
val posY = posmap.y[index].flipY() + val posY = posmap.y[index].flipY() +
if (sheetID == SHEET_UNIHAN) // evil exceptions if (sheetID == SHEET_UNIHAN) // evil exceptions
offsetUnihan offsetUnihan
@@ -625,13 +606,8 @@ class TerrarumSansBitmap(
else 0 else 0
val posX = posmap.x[index] val posX = posmap.x[index]
val texture = sheets[sheetID].get(sheetX, sheetY) atlas.getRegion(sheetID, sheetX, sheetY)?.let {
linotypePixmap.drawFromAtlas(atlas.pixmap, it, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
}
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
} }
} }
@@ -715,24 +691,21 @@ class TerrarumSansBitmap(
val (indexCho, indexJung, indexJong) = indices val (indexCho, indexJung, indexJong) = indices
val (choRow, jungRow, jongRow) = rows val (choRow, jungRow, jongRow) = rows
val hangulSheet = sheets[SHEET_HANGUL] 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 {
val choTex = hangulSheet.get(indexCho, choRow) linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
val jungTex = hangulSheet.get(indexJung, jungRow) }
val jongTex = hangulSheet.get(indexJong, jongRow) atlas.getRegion(SHEET_HANGUL, indexJong, jongRow)?.let {
linotypePixmap.drawFromAtlas(atlas.pixmap, it, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
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)
index += hangulLength - 1 index += hangulLength - 1
} }
else { else {
try {
val posY = posmap.y[index].flipY() + val posY = posmap.y[index].flipY() +
if (sheetID == SHEET_UNIHAN) // evil exceptions if (sheetID == SHEET_UNIHAN) // evil exceptions
offsetUnihan offsetUnihan
@@ -741,13 +714,8 @@ class TerrarumSansBitmap(
else 0 else 0
val posX = posmap.x[index] val posX = posmap.x[index]
val texture = sheets[sheetID].get(sheetX, sheetY) atlas.getRegion(sheetID, sheetX, sheetY)?.let {
linotypePixmap.drawFromAtlas(atlas.pixmap, it, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
}
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
} }
} }
@@ -826,7 +794,7 @@ class TerrarumSansBitmap(
override fun dispose() { override fun dispose() {
super.dispose() super.dispose()
textCache.values.forEach { it.dispose() } textCache.values.forEach { it.dispose() }
sheets.forEach { it.dispose() } atlas.dispose()
} }
fun getSheetType(c: CodePoint): Int { 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() = private fun Color.toRGBA8888() =
(this.r * 255f).toInt().shl(24) or (this.r * 255f).toInt().shl(24) or
(this.g * 255f).toInt().shl(16) or (this.g * 255f).toInt().shl(16) or

Binary file not shown.

Binary file not shown.

Binary file not shown.