Game UI Asset Lab — Slice a UI Sheet into a Padded Atlas
One workspace for game UI art: find the elements on a sheet, pack them with padding and extrude, give panels nine-slice borders, and check them at the resolutions they will really be drawn at.
A UI sheet (buttons, panels and icons drawn on one transparent PNG) has to become separate, named rectangles before an engine can draw any of it. The UI Lab finds every element as an island of pixels above an alpha threshold, joins islands closer than the merge distance (a button and the badge beside it), packs the elements into ui-atlas.png with padding and optional extrude, and writes each rect and any nine-slice border to ui-atlas.json. It writes numbers, not Godot or Unity resource files; the 9-Slice, States, Font and Check stages work on the same image.

- Nine-sliceMeasured· Pillow
- Bitmap fontMeasured· independent .fnt parser
- Button statesMeasured· Pillow
- Engine importUNVERIFIED
From a UI sheet to atlas rectangles
An element is found the way you would outline it by hand. Pixels whose alpha is above the threshold (default 8 of 255) are grouped with their eight neighbours into islands, and islands smaller than the minimum size (default 16 pixels) are dropped as specks of dust.
Islands are candidates, not meaning. An icon with a separate dot next to it is two islands; the merge distance (default 4 px) unions any two boxes whose gap is at most that many pixels and repeats until nothing merges, so a chain of nearby pieces becomes one element tagged merged. Two buttons that touch are a single island and stay one element whatever the merge distance.
Packing places the elements on one page of at most 4096 × 4096 px with MaxRects and leaves the padding (default 2 px) free around them. Extrude repeats each element's outermost pixels outward by N px, so a filtered or mipmapped sample that lands just outside the element still reads the element's own colour. The rect in the JSON is the element itself; the extruded ring lies outside it.
- Alpha island
- A group of 8-connected pixels above the alpha threshold.
- Merge distance
- The largest gap, in pixels, at which two boxes become one element.
- Padding
- Empty pixels left between packed elements.
- Extrude
- Copies of an element's edge pixels placed around it, outside its rect.
Example: one 256 × 128 sheet, measured
Five shapes on a transparent sheet, run through the same detection, merge and packing code the Lab uses:
sheet 256 × 128 alpha threshold 8 minimum 16 px merge distance 4
islands panel 64×64 @8,8 button 96×32 @96,8 icon 16×16 @200,12 dot 4×4 @218,12 arrow 20×20 @96,64
gap icon → dot = 218 − (200 + 16) = 2 px ≤ 4 → one element 22×16 @200,12, tag "merged"
result 5 islands → 4 elements (merge distance 0 would keep 5)
pack padding 2, extrude 1 → ui-atlas.png 102 × 108
rects element-02 3,3 96×32 element-01 3,41 64×64 element-03 73,41 22×16 element-04 73,63 20×20Elements are numbered top to bottom, then left to right, before packing: element-01 is the panel, element-03 the merged icon. Rename them before export, because the names become the keys under frames in the JSON.
What it does
- Element detection from alpha islands with a merge distance (a button and its glyph become one element).
- UI atlas: MaxRects packing with padding and extrude; borders set per element travel into the JSON.
- Stages for nine-slice, button states, bitmap fonts and checks (sizes and anchors, safe areas, localisation overflow, WCAG contrast).
- Settings links: every non-image setting can be shared as a URL.
How to do it
- Drop the UI sheet.
- Review the detected elements, rename them, adjust the merge distance.
- Give panels nine-slice borders.
- Export the atlas PNG + JSON.
Check the result
- Open
ui-atlas.json:frameshas one entry per element you kept, and everyrectlies insidemeta.size. - Crop one
rectout ofui-atlas.pngin any image editor and compare it with the same element on the source sheet; they are the same pixels. - Look for
"tag": "merged": each one should be a piece you meant to join, not two buttons that happen to sit close together. - If you gave an element nine-slice borders,
SETUP.mdhas a section with its name and the four numbers.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Two buttons came out as one element | They touch, or the gap between them is not larger than the merge distance | The element's box covers both buttons; the tag says merged when it came from more than one island | Set the merge distance to 0; if they touch, leave at least one transparent pixel between them in the art |
| A label, glow or shadow became its own element | Its pixels are separated from the button by more than the merge distance, or they are fainter than the alpha threshold | Count the elements against what you expect; zoom into the gap | Raise the merge distance a few pixels; lower the alpha threshold if a soft shadow is being cut off |
| Tiny specks appear as elements | Stray pixels form islands above the minimum size | Very small rects in the element list | Raise the minimum pixels, or erase the dust in the art |
| Thin lines of a neighbour appear at an element's edge in the engine | The engine filters or mipmaps across the rect border and padding 0 lets it read the neighbour | Zoom in on the edge in the running game | Use padding 2 and extrude 1–2, or Nearest filtering for pixel art |
| The atlas fails to pack | The elements do not fit on one 4096 × 4096 page | The summary shows the error instead of a size | Split the sheet and build two atlases; there is no multi-page UI atlas |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| A packer for separate PNG files (sprite sheet packer, TexturePacker) | Each element already exists as its own file: no detection is needed, and a packer writes engine-specific atlas formats. See packing without TexturePacker. |
| Unity's Sprite Editor slicing | The sheet is only used in Unity and you want the rects stored in Unity's own import settings. |
Godot AtlasTexture resources set by hand | Two or three elements: point atlas at the sheet and type each region; no extra file at all. |
Outputs and how each was checked
"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.
| Target | Files | Check |
|---|---|---|
| Nine-slice | nine-slice.json · previews · SETUP.md | Measured PillowEvery resized preview keeps all four corner blocks unscaled: the same bytes in Chromium; in Firefox semi-transparent pixels can shift by one alpha-rounding step. 0 gaps, 0 overlaps. |
| Bitmap font | font.png · font.fnt (BMFont text) · font.json | Measured independent .fnt parserThe .fnt was read back by a parser written separately in the test: header and every glyph rect match. |
| Button states | states/*.png · strip · states.json | Measured PillowNormal equals the source (byte for byte in Chromium; in Firefox semi-transparent colour within one alpha-rounding step); every rect in states.json cuts exactly its state out of the strip. |
| Engine import | Godot NinePatchRect · Unity Sprite border · BMFont import | UNVERIFIEDNot run in Godot or Unity. |
Evidence
Nine-slice corners unscaled at 100×40, 300×80, 800×200 and 420×120; border suggestion 6·6·6·6 from a run of 12 identical columns; button states: normal equal to the source, disabled grey at alpha 128, focus ring exact; .fnt glyph records equal the JSON; missing glyphs from a .po listed exactly; black on white measured 21:1. Images pass through a browser canvas: Chromium returns the same bytes; Firefox rounds the colour of semi-transparent pixels to one premultiplied step (measured on a Kenney button: up to 4 levels at alpha 63), while alpha and opaque pixels stay exact.
Limits
- One atlas page, at most 4096 × 4096, and no rotated packing.
- No engine resource files: Godot
.tresor Unity.metaare not written, only rects and border numbers. - No kerning pairs, one font page, no MSDF; the SDF output is Beta.
- A TTF's cmap is not parsed, so the missing-glyph check needs a .fnt or the Lab's own font.
- One atlas page; touching elements count as one.
- Rendering inside Godot or Unity was not run: the engine numbers (patch margins, L-B-R-T border) are documented, not engine-tested.
Versions and sources
Measured in Chromium (Playwright) on Windows 11 for docs/UI-LAB.md: a 128 × 64 sheet with one button and a transparent hole gave 4 elements that merge distance 40 joins into one; packed pixels equal their source rects and extrude 1 duplicates the edge pixel outside the rect; borders 5/5/4/4 set on an element reach ui-atlas.json; a 1024 × 1024 sheet with 40 elements was detected, merged, packed and drawn in 76 ms. Loading the atlas in Godot or Unity was not run. Engine names above follow the Godot 4.7 documentation.
Official documentation used
Questions
Why are two touching buttons one element?
Elements are alpha islands; touching art is one island. Separate them in the art, or cut the result in the nine-slice stage.
Does it write Godot or Unity resources?
No. It writes PNG + JSON with the numbers each engine needs (patch margins, L-B-R-T border) and a SETUP.md; engine import was not tested.
Is it free, and do I need an account?
Everything on these pages is free and works without an account. There is no watermark and no export that is held back.
Are my sprites uploaded?
No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.
Does it need Aseprite or an engine plugin installed?
No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).