Dynamically packing Textures in LibGDX with PixmapPacker
In Unciv, we’re very big on modding. Many of our players use mods that change the images we use to render on the screen, and since each mod packs its own images this causes Texture swaps and thus high latency (see: Minimize Texture Swapping).
The solution? Pack textures at runtime with PixmapPacker for peformance bottlenecks!
We can even go one step further — since many of these images are “enhanced” with other images in-game (surrounded with circles, added icons, etc) we can pre-render these into a single image so there’s only one draw() call.
Step 1 — Generating Pixmaps from Actors
This is a trick we figured out when adding emojis to our fonts.
We draw() the actor with a SpriteBatch onto a FrameBuffer and then read the pixels directly from OpenGL.
Since the GL Y axis is opposite to the LibGDX Y axis, actors need to be flipped upside down first — this is handled in step 2.
private val frameBuffer by lazy {
FrameBuffer(Pixmap.Format.RGBA8888, Gdx.graphics.width, Gdx.graphics.height, false)
}
private val spriteBatch by lazy { SpriteBatch() }
private val transform = Matrix4() // for repeated reuse without reallocation
/**
* Draws onto an offscreen frame buffer and copies the pixels.
* Caller becomes owner of the returned Pixmap and is responsible for disposing it.
*/
fun getPixmapFromActor(actor: Actor): Pixmap {
val pixmap = Pixmap(boxWidth, boxHeight, Pixmap.Format.RGBA8888)
frameBuffer.begin()
Gdx.gl.glClearColor(0f,0f,0f,0f)
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
spriteBatch.begin()
actor.draw(spriteBatch, 1f)
spriteBatch.end()
Gdx.gl.glReadPixels(0, 0, boxWidth, boxHeight, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixmap.pixels)
frameBuffer.end()
return pixmap
}
Step 2 — Create your Actors and pack them
Since we have many different types of actors, each used in a different layer, we want a generic function that gets a list of actors with names so we can store them all in our large hashmap of names-to-texture-regions
// We put all the drawables into a hashmap, because the atlas specifically tells us
// that the search on it is inefficient
private val textureRegionDrawables = HashMap<String, TextureRegionDrawable>()
private fun packTexture(nameToActorList: List<Pair<String, Group>>, size: Int) {
val pixmapPacker = PixmapPacker(2048, 2048, Pixmap.Format.RGBA8888, 2, false).apply { packToTexture = true }
for ((name, actor) in nameToActorList) {
actor.apply { isTransform = true; setScale(1f, -1f); setPosition(0f, height) } // flip Y axis
val pixmap = FontRulesetIcons.getPixmapFromActorBase(actor, size, size)
pixmapPacker.pack(name, pixmap)
pixmap.dispose()
}
val yieldAtlas = pixmapPacker.generateTextureAtlas(
TextureFilter.MipMapLinearLinear,
TextureFilter.MipMapLinearLinear,
true
)
for (region in yieldAtlas.regions) {
val drawable = TextureRegionDrawable(region)
textureRegionDrawables[region.name] = drawable
}
pixmapPacker.dispose()
}
Example use:
private fun setupResourcePortraits() {
val nameToActorList = ruleset.tileResources.values
.map { it.name to getResourcePortrait(it.name, 100f, borderSize = 10f) }
packTexture(nameToActorList, 120)
}
And that’s it!
You can now replace the usage of the Actors with an Image(textureRegionDrawables[name])
and it will be rendered from the packed texture.
Note that the resolution of the prerendered image, and not the original image, now determines the result — so choose your Actor sizes accordingly.
Happy rendering!