June 12th, 2026

Dual-grid tilesets, explained

Why autotiling on one grid needs 47 tiles, and how drawing tiles between the cells instead gets perfect corners with 15. With diagrams and the mask table.

Dual-grid tilesets, explained

Dual-grid tilesets, explained

You paint terrain cells, and the tilemap picks the right tile for each one. That's autotiling. The standard way of doing it needs 47 hand-drawn tiles. The dual-grid way needs 15, and the corners come out better. The whole difference is where the tiles are drawn.

This post is the deep version of how that works. If you just want the short story of what a 15-piece sheet is, start with the 15-piece autotile, explained and come back.

The single-grid approach, and where it breaks

The obvious setup puts the tiles exactly where you paint. Cell (3,5) is grass, so a grass tile gets drawn at (3,5), and which grass tile depends on the neighbors: grass to the right means no edge on the right, and so on.

How many neighbors do you check? The cheap version checks four, the cells sharing an edge:

A grid cell with its four edge neighbors highlighted
4 edge neighbors: 2^4 = 16 combinations
A grid cell with all eight neighbors highlighted
8 neighbors: 256 combinations, 47 distinct tiles

Four neighbors gives you 16 tile variants, which sounds fine until a diagonal shows up. Two grass cells touching only at a corner have identical edge-neighbor patterns as two grass cells not touching at all, so the tilemap literally cannot tell the difference. Inner corners are unsolvable with edge information alone.

So you check all eight neighbors. Now the tilemap can see diagonals, but the combination count explodes to 256. After removing cases that look identical you're left with 47 visually distinct tiles, the classic "blob" tileset. It works. It also means drawing 47 variations of dirt, and most engines make you wire up the matching rules for every one of them.

The dual-grid idea

Dual grid changes one thing: the tiles aren't drawn on the cells you paint. They're drawn on a second grid, offset by half a tile, so every rendered tile sits across exactly four painted cells.

Painted terrain cells on a gray grid, with an orange render grid offset by half a tile, and one highlighted render tile overlapping four cells

Green is what you painted. Orange is where tiles actually get drawn. The highlighted tile overlaps four painted cells, marked with dots, and those four cells are all it needs to know about: each one is either ground or not.

That's the trick. A rendered tile's corners each touch one painted cell, so its appearance is fully determined by four yes/no questions. Two options, four corners, 2^4 = 16 cases. One case is "all four empty", which draws nothing. The other 15 are the 15-piece tileset.

Compare the bookkeeping: the single-grid blob needs 8 neighbors and 47 tiles to handle what dual grid covers with 4 cells and 15 tiles. Nothing is approximated, every case is exact, the sheet is just smaller because the question is smaller.

The diagonal, solved for free

Remember the case that broke edge-matching? Two cells touching at a corner. On the dual grid, the rendered tile sitting between them samples one filled cell at its top-left and one at its bottom-right, a combination that exists in the sheet as its own piece. The diagonal isn't a special case, it's just mask number 9.

Two diagonal cells rendered as full tiles touching only at a corner point
Stamping full tiles: the cells touch at a zero-width point
The same two diagonal cells rendered with the dual grid, connected by a proper diagonal piece
Dual grid: the in-between tile is a real diagonal piece

What a finished map looks like

Here's a full map rendered dual-grid style, with the painted-cell grid drawn over it in green. Notice that every tile boundary sits halfway between the green lines: the art lives between the cells, which is why every edge and corner lands on a piece that was drawn for exactly that situation.

A cobblestone map rendered with the dual grid, with the painted-cell grid overlaid in green to show tiles sitting between cells

One practical consequence: the rendered map is one tile wider and taller than the painted map, because tiles also sit around the outside edge of the painted cells. If your renderer loops over the map size and the border looks chopped off, that's the missing row and column.

The mask, if you're implementing it

Each rendered tile turns its four cells into a 4-bit mask. SpriteCook's bit order:

let mask = 0
if (filled(x - 1, y - 1)) mask |= 1 // top-left
if (filled(x,     y - 1)) mask |= 2 // top-right
if (filled(x - 1, y    )) mask |= 4 // bottom-left
if (filled(x,     y    )) mask |= 8 // bottom-right

The mask picks a cell from the 4x4 sheet through one lookup table:

const frameByMask = [-1, 15, 8, 9, 0, 11, 14, 7, 13, 4, 1, 10, 3, 2, 5, 6]
const frame = frameByMask[mask] // -1 = draw nothing
const atlasColumn = frame % 4
const atlasRow = Math.floor(frame / 4)

Mask 0 draws nothing, mask 15 is the filled interior, and mask 9 is that diagonal from earlier. That's the entire runtime: no rule trees, no per-tile configuration, one table.

If you'd rather not write a renderer at all: the engine exports hand Godot, Unity, and Tiled files with the terrain matching already wired up, so the dual-grid logic only matters when you're rolling your own tilemap.

Or let your agent implement it

If you code with an AI agent, this whole post is already in its hands. The SpriteCook plugin for Claude Code, Codex, Cursor, and OpenCode ships with a dual-grid skill containing exactly this implementation: the mask order, the lookup table, the renderer. Tell your agent to add terrain to your game and it can generate the tileset and wire up this renderer in your codebase in one go. Setup takes a couple of minutes on the agents page.

Where to next

Got questions? Building something?

Drop into the Discord. People post WIPs, share prompts that actually worked, and help each other debug weird outputs. We're in there daily too.

Join the Discord