SpriteForge.tech
Play the demo Blog Pricing Open the studio

How an LLM draws pixel art

← All posts · · 11 min read
TL;DR
  • No image model. Claude writes the sprite as text: one palette letter per pixel, on the final grid. Code turns that text into a PNG.
  • Most of the reliability is in the plumbing: a text format built around how language models read and write, a decoder that expects miscounts, and rendered images fed back to the model between passes.
  • Measured: a 32×32 sprite costs about $0.02 to $0.20 in model fees and takes one to two minutes on the default tiers. It is strongest at 16 to 32 px and still weak on intricate creatures, bigger canvases and varied tiles.

SpriteForge makes game sprites and tilesets at 8 to 64 pixels. The usual way to generate images is diffusion; SpriteForge doesn't use it. A language model places every pixel itself, as text, and the rest of the system exists to make that work reliably. This post covers how: the format the model writes, the decoder that reads it, how the model checks its own work, how tilesets line up, what it costs, and where it still breaks.

Why not diffusion at 16 to 32 px

A diffusion model paints a large image and the "pixel art" step comes afterwards: downscale, snap to a grid, reduce the colours. At 16 or 32 pixels that leaves three problems you can measure.

A king slime from a diffusion pixel-art generator, magnified 3 times: a crown with gems and a blue body covered in dither and speckle
Diffusion
51×63 px · 29 colours
A SpriteForge slime, magnified 3 times: green, flat shading, one highlight and a dark outline
SpriteForge
20×16 px · 7 colours
Both shown pixel for pixel at 3×, nothing resampled. Colour counts are the unique opaque colours in each file. More on these tells in How to spot AI pixel art.

Diffusion is good at large, detailed images. The trouble is specific to small sprites, where every pixel shows and a set of assets has to share one palette. That is the range SpriteForge is built for.

Pixels as text

The model never produces an image. It writes a block like this, and code draws it:

GRID
................
................
................
..zAAAz..zAAAz..
.zABBCAzzADDEAz.
.ABBCCDEEDDDEEA.
.ABCCDDDDDDDDEA.
.ACCDDDDDDDDDEA.
.zACDDDDDDDDEAz.
..zADDDDDDDEAz..
...zADDDDEEAz...
....zADDEEAz....
.....zADEAz.....
......zAAz......
................
................
PALETTE
z=23,22,29
A=53,36,50
B=242,229,205
C=245,138,88
D=220,73,56
E=152,36,48
A 16×16 heart from the studio's demo project as a grid of palette letters, one per pixel. The picture beside it is drawn by your browser from that text.

What you can see in that block is there for a measured reason:

The format the model actually writes has a few more tricks that make it more native to how a language model reads and writes text. Those we keep to ourselves.

The cost is verbosity: about 1.42 tokens per cell, so a 64×64 grid is roughly 5,800 tokens. We tried denser encodings. On a real 32×32 sprite, our format took 1,452 tokens where a compact one took 617 and run-length encoding took 387. The token count was the wrong thing to optimise. In a blind test on the previous Opus generation (4 subjects, best of 3), our format produced 34 recognisable sprites out of 36, compact 30 and run-length 28. Compact even cost more in total ($3.07 against $2.79), because the model spent the saved tokens reasoning about where it was. Run-length saved 21%. Our format stayed.

Coordinate lists, one x,y,colour per pixel, are about 2.5 times worse than the grid for a whole sprite. They are good for small edits, though: when you repaint a selection covering up to a quarter of the sprite, the model returns only the changed pixels as x,y=letter lines, so nothing outside the selection can move.

A decoder that expects mistakes

Language models are bad at counting to 64. In early versions, rows meant to be 64 cells wide came back between 61 and 70 wide, and in one run 25 of 63 rows were the wrong width. If the decoder can only pad or trim at the end of a row, every counting slip piles up along the right side of the sprite.

Part of the fix is in the format itself: it helps the model keep its place along a row, and it helps the decoder find where a row went wrong. With it, off-width rows went to zero in testing. The decoder works roughly like this:

  1. Strip anything that isn't a cell: row numbers, code fences, stray headers.
  2. If the row already has exactly the right number of cells, take it as written. A correct row is never "repaired".
  3. Otherwise, fix the row where it went wrong: trim a stretch that runs long, and extend one that runs short by copying its final cell. Padding with transparent pixels instead cut thin vertical strips through sprites, and trimming without padding made a sword's blade wobble from row to row.
  4. Check the palette. Because the palette is written after the grid, a reply cut off by the token limit leaves letters with no colour. If more than a few cells are affected, the generation fails and is refunded instead of shipping a grey sprite.

Letting the model look at its work

A model can't judge pixel art from a wall of letters. So in the multi-pass recipes, the server renders each pass into an enlarged PNG with labelled axes and attaches it to the next request. Drawing the picture is the server's job, not the model's. The passes split the work:

  1. Reframe, plan, silhouette. Settle what exactly is being drawn, work out its parts and their sizes, then block in the shape.
  2. Detail and colour. Surface features, then shading from one light direction.
  3. Touch-up. Inspect the render region by region and fix only real defects.

The image is what makes the later passes useful. In research runs, self-review from the text alone gave roughly no improvement, while the same review with the render attached did. The image is also cheap: a render of about 512 pixels costs 350 to 500 input tokens.

When this loop was validated, its best roll was identified by all three blind judges on each of the six benchmark subjects, while a single pass broke down on the hardest 32 px subjects: a knight came out as a robot, and a chest as nothing recognisable. A later re-run on an earlier model found the two-pass split (silhouette, then detail and colour) as good as three passes: the overall scores were within 0.12 of each other. In the studio, Single pass is the default; the two- and three-pass recipes are under the advanced options. If a later pass fails, you get the last good pass and the unused passes are refunded.

Plan before silhouette

The silhouette is where small sprites succeed or fail, so the plan comes first and is explicit. The model writes down anatomy, proportions, aspect ratio and a palette arranged as a light ramp (outline, shadow, body, lit, highlight) before drawing. Aspect gets its own rule: a wide, low subject must stay wider than it is tall, because the wrong aspect reads as a different object.

Two rules came out of failures:

Reasoning effort matters less in the plan than you might guess. In a test on five 32 px subjects, spending more reasoning on the planning pass and less on the later ones cost about twice as much as low effort throughout and looked about the same. The quality that medium effort buys came from the detail, colour and touch-up passes. Cutting effort on the silhouette also has a cost: one key came back with its bow squashed into an oval, and the later passes shaded the wrong shape faithfully.

Tilesets that line up by construction

A tileset is harder than a sprite because the edges have to agree. In a 16-tile Wang set (the dual-grid autotile layout Godot, Unity and Tiled terrain brushes understand), each tile shows terrain in some combination of its four corners, and every pair of neighbouring tiles has to meet without a seam.

SpriteForge doesn't ask the model to draw 16 tiles. It asks for one seamless texture, in one call, and derives the 16 tiles from it in code. Each tile is the same texture, cut by a field computed from its four corners. Neighbouring tiles share two corners, so along their shared edge the field is identical and the boundary lines meet exactly. The texture is sampled at the same positions in every tile, so wherever the texture itself wraps, the pixels continue across too. The tile boundaries never depend on the model getting an edge right.

SpriteForge fill
two copies, 8× · 5 colours
Retro Diffusion tile
two copies, 8× · 26 colours
The prompt "cobblestone road", one 16×16 tile from each tool, unedited, drawn twice side by side. The SpriteForge tile is from an earlier quality tier.
The SpriteForge tileset editor: a painted test map where grass patches meet dirt, the base texture, and the 16 Wang tiles derived from it with gold dots marking their painted corners
A grass-on-dirt Wang set in SpriteForge's tileset editor. The base texture is the only thing the model drew; the 16 tiles below it are derived, and editing the texture re-derives them.

What is still up to the model is that the texture wraps: its right edge has to continue into its left, and its bottom into its top. Grass, water and gravel, with lots of small detail, wrap well. Large rigid features don't. At 16 px, cobblestones failed to line up across the wrap, and rerolling didn't help, which points at a capability limit rather than bad luck.

For a wall sitting on a floor, there is no way around asking the model to match an edge, so the request makes it as easy as possible. The model sees the wall and floor tiles both as images and as letter grids, with their palettes merged and any clashing letters renamed first, and is told that its top row must equal the wall's bottom row letter for letter, and its bottom row the floor's top row. Seeing the rows as letters is what makes the rule possible to follow. In the 16 px research runs this joined wall to floor in every run on both models tested.

What it costs and how long it takes

These are model fees for one single-pass sprite, from a benchmark on 15 October 2026 that ran the production prompt on three subjects per size. Cost is measured tokens at list price; time is the model call alone.

Tier (medium effort)16×1632×3264×64
Claude Haiku 5.5$0.006 · 52 s$0.016 · 120 s$0.021 · 163 s
Claude Sonnet 5.5$0.027 · 20 s$0.086 · 56 s$0.178 · 105 s
Claude Opus 5.5$0.058 · 25 s$0.200 · 83 s$0.403 · 156 s
Mean of 3 runs per cell. Source: MEASURED_COST and MEASURED_TIME_S in the engine's cost table.

Effort moves both numbers. At 32 px, Opus on low effort averages $0.099 and 43 seconds, and on high effort $0.284 and 121 seconds. Some notes on reading the table:

The studio charges four times the measured mean per generation, with a floor so that a higher effort never costs less than a lower one.

Where it fails

Why bother

Because the output can't be anything but pixel art. Every pixel is on the grid and every colour comes from a palette that was chosen, so there is nothing to snap or re-quantise afterwards. The work goes into getting a text model to make good decisions inside those constraints, and most of the gains so far came from unglamorous things: the token format, a careful decoder, and showing the model a picture of what it drew.

This work is also the subject of the author's bachelor's thesis.

If you want to try it on your own prompt, the studio is at spriteforge.tech/create.