technique

Isometric pixel art

How to draw on the 2:1 pixel line, build the diamond tile and the three-face cube, place and sort tiles from their map coordinates, and keep line angle and light consistent.

Before this

This page assumes you are comfortable with:

Why you need this

Isometric is the view that shows a top face and two side faces of everything at once, which is why strategy games, city builders, and dungeon crawlers use it: the player reads depth and height without a moving camera. In pixel art it comes with one unbreakable rule, the 2:1 line, and a small amount of arithmetic to place tiles on screen. This page belongs to stage 2 of the pipeline, draw the still, and its placement math feeds stage 4, build the world. Read Projections and views first for what isometric shows and hides compared with side view and top-down.

The idea

The 2:1 line

True isometric projection puts the ground axes at 30 degrees from horizontal. A pixel line at 30 degrees has an uneven stair pattern that looks broken at 1x. Pixel isometric instead uses a line that rises 1 pixel for every 2 pixels across, about 26.6 degrees. Every ground edge in the whole game is drawn with this stair, two across then one up, and nothing else: not 1:1, not 3:1, not a run of three here and a run of two there.

. . . . . . 1 1
. . . . 1 1 . .
. . 1 1 . . . .
1 1 . . . . . .

1 line, . empty. Two pixels across, one up, repeated.

Because every run is exactly 2, the line is a rhythm the eye locks onto, and any run of 1 or 3 in a ground edge reads as a bump.

The tile diamond

An isometric ground tile is a diamond twice as wide as it is tall, because that is the shape a square makes when drawn with the 2:1 line. Call the tile width wtw_t and the tile height ht=wt/2h_t = w_t / 2. Common sizes are 32x16 and 64x32; this page draws at 16x8 so the grids fit. The width must be a multiple of 4, so that both wt/2w_t / 2 and ht/2h_t / 2, which the placement math needs, are whole pixels.

The filled diamond, with its four edges labeled:

. . . . . . a a b b . . . . . .
. . . . a a t t t t b b . . . .
. . a a t t t t t t t t b b . .
a a t t t t t t t t t t t t b b
. . d d t t t t t t t t c c . .
. . . . d d t t t t c c . . . .
. . . . . . d d c c . . . . . .
. . . . . . . . . . . . . . . .

t top face, a upper-left edge, b upper-right edge, d lower-left edge, c lower-right edge, . transparent.

Row widths are 4, 8, 12, 16, 12, 8, 4: 64 pixels, and the eighth row is empty. That is not an accident. Neighbors sit at offsets of (±8,4)(\pm 8, 4), and 64 pixels is exactly the area each tile owns in that arrangement, so these diamonds fit together with no gaps and no overlap. If you draw a symmetric outline with two rows of width 16, the widest rows of neighboring tiles overlap by one row, which is fine for outlines drawn back to front and wrong for opaque fills.

The cube

A block is a diamond with two faces hanging under it. Draw the top diamond, then from its lower-left edge (d) drop the left face, and from its lower-right edge (c) drop the right face, each a parallelogram hh pixels tall. Shade the three faces with three values from one ramp. With light from the upper left, the top face is the lightest, the left face is mid, and the right face is darkest.

A 16x8 cube with h=8h = 8, on a 16x16 canvas:

. . . . . . 3 3 3 3 . . . . . .
. . . . 3 3 3 3 3 3 3 3 . . . .
. . 3 3 3 3 3 3 3 3 3 3 3 3 . .
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
2 2 3 3 3 3 3 3 3 3 3 3 3 3 1 1
2 2 2 2 3 3 3 3 3 3 3 3 1 1 1 1
2 2 2 2 2 2 3 3 3 3 1 1 1 1 1 1
2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1
2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1
. . 2 2 2 2 2 2 1 1 1 1 1 1 . .
. . . . 2 2 2 2 1 1 1 1 . . . .
. . . . . . 2 2 1 1 . . . . . .
. . . . . . . . . . . . . . . .

3 top #cbbfae, 2 left face #9a8a86, 1 right face #6b5a66, . transparent.

Three checks. Every slanted edge is 2:1. The vertical edges (columns 0 and 1 on the left, 14 and 15 on the right, and the split between columns 7 and 8 down the middle) are straight. The three values are three distinct steps of one ramp, not one color at three brightnesses picked by eye. The bottom seven rows of the cube are a diamond the same shape as the top, which is the cube's footprint on the ground; the cube image is wtw_t wide and ht+hh_t + h tall, and you draw it hh pixels above where the ground tile would go so that footprint lands on the tile. A unit cube has h=wt/2h = w_t / 2, so a 32x16 tile gets a 16-pixel-tall cube.

Placing tiles on screen

Map coordinates are (tx,ty)(t_x, t_y): txt_x increases toward the lower right of the screen, tyt_y toward the lower left. Screen coordinates are (x,y)(x, y), xx right, yy down, origin top-left, and the formulas place the top-left corner of the tile's bounding box:

x=(tx−ty) wt2,y=(tx+ty) ht2x = (t_x - t_y)\,\frac{w_t}{2}, \qquad y = (t_x + t_y)\,\frac{h_t}{2}

For a 32x16 tile, wt/2=16w_t / 2 = 16 and ht/2=8h_t / 2 = 8. Tile (2,1)(2, 1) lands at x=(2−1)⋅16=16x = (2 - 1) \cdot 16 = 16 and y=(2+1)⋅8=24y = (2 + 1) \cdot 8 = 24. Since xx goes negative when ty>txt_y > t_x, add an origin offset of (Hm−1)⋅wt/2(H_m - 1) \cdot w_t / 2, where HmH_m is the map height in tiles, so the leftmost tile starts at x=0x = 0.

Draw order

Tiles and sprites overlap, so they must be drawn back to front, which is the painter's algorithm. The back of an isometric map is the top of the screen, where tx+tyt_x + t_y is small. Sort everything by tx+tyt_x + t_y ascending, and within one cell by height ascending, so a block is drawn before the block stacked on it. A sprite standing on a tile sorts by that tile's (tx,ty)(t_x, t_y), not by its own pixel position.

const W = 32, H = 16;                          // tile size in pixels
function toScreen(tx, ty, z, mapH) {
  const originX = (mapH - 1) * (W / 2);
  return { x: (tx - ty) * (W / 2) + originX, y: (tx + ty) * (H / 2) - z * (H / 2) };
}
function drawOrder(things) {                    // things: [{tx, ty, z, ...}]
  return [...things].sort((a, b) => (a.tx + a.ty) - (b.tx + b.ty) || a.z - b.z);
}
toScreen(2, 1, 0, 3);   // { x: 48, y: 24 }

Height and stacking

One elevation level is ht/2h_t / 2 pixels: a thing at level zz is drawn z⋅ht/2z \cdot h_t / 2 pixels higher on screen than the same thing at level 0, which is the −z⋅ht/2-z \cdot h_t / 2 term in the code. A unit cube is two levels tall, since its vertical edge is hth_t, so a block stacked on a block sits at z=2z = 2 and is drawn hth_t pixels higher. A half-height step or a ramp is one level. Nothing about a thing's map coordinates changes with height; only its screen yy and its sort tie-break do.

Curves and circles

A circle lying on the ground becomes an ellipse exactly twice as wide as it is tall, because the ground plane is squashed 2:1. A round well, a pond, a coin on the floor: all 2:1 ellipses. The runs along an ellipse are not constant, they shorten toward the sides, which is how you tell a drawn ellipse from a diamond drawn lazily:

. . . . . 1 1 1 1 1 1 . . . . .
. . . 1 1 . . . . . . 1 1 . . .
. 1 1 . . . . . . . . . . 1 1 .
1 . . . . . . . . . . . . . . 1
1 . . . . . . . . . . . . . . 1
. 1 1 . . . . . . . . . . 1 1 .
. . . 1 1 . . . . . . 1 1 . . .
. . . . . 1 1 1 1 1 1 . . . . .

1 outline, . empty. A 16x8 ellipse; runs of 6, 2, 2, 1 from the top down to the widest row.

A vertical cylinder, a tower or a barrel, is an ellipse on top, two straight vertical sides, and the lower half of an ellipse at the bottom, shaded with the ramp running from light on the upper-left side to dark on the right.

Characters

Characters in isometric games are almost never drawn in true isometric. They are drawn upright, as in a side view or three-quarter view, standing on the tile's center, with their pivot at the bottom-center of the sprite. What makes them belong is the facing set: 4 directions (along the four tile edges) at minimum, 8 if the game moves diagonally, each with its own walk cycle. A 32x16 tile usually carries a character 24 to 40 pixels tall; taller than the tile is normal, since the character stands on it rather than inside it. See Character design for sprites for the silhouette work.

Worked example

A 3x3 map of 32x16 tiles, Hm=3H_m = 3, so the origin offset is (3−1)⋅16=32(3 - 1) \cdot 16 = 32. Screen position of each tile's bounding-box corner at level 0:

(tx,ty)(t_x, t_y) tx−tyt_x - t_y tx+tyt_x + t_y xx raw xx with offset yy draw order
(0, 0) 0 0 0 32 0 1
(1, 0) 1 1 16 48 8 2
(0, 1) -1 1 -16 16 8 3
(2, 0) 2 2 32 64 16 4
(1, 1) 0 2 0 32 16 5
(0, 2) -2 2 -32 0 16 6
(2, 1) 1 3 16 48 24 7
(1, 2) -1 3 -16 16 24 8
(2, 2) 0 4 0 32 32 9

The whole map is 96 pixels wide, three tiles across its widest row, and 32+16=4832 + 16 = 48 pixels tall. Tiles with the same sum, such as (2,0)(2, 0) and (1,1)(1, 1), sit side by side at the same yy and never overlap, so their relative order does not matter.

Put a unit cube on (1,1)(1, 1). Its image is 32 wide and 16+16=3216 + 16 = 32 tall, drawn 16 pixels above the ground tile, at (32,0)(32, 0). Its top diamond covers parts of the ground tiles at (0,0)(0, 0), (1,0)(1, 0), and (0,1)(0, 1), which have sums 0 and 1 and were drawn earlier, so the cube correctly hides them. Its two faces fit exactly against the diamonds of (2,1)(2, 1) and (1,2)(1, 2) without overlapping them, which is the tessellation from the diamond section doing its job. Stack a second cube on the first at z=2z = 2: it draws at (32,0−16)=(32,−16)(32, 0 - 16) = (32, -16), after the first because of the tie-break, and the negative yy says the map needs a top margin at least as tall as its tallest stack.

In a game's art pipeline

Isometric is a stage 1 decision before it is a stage 2 drawing: choosing it fixes the tile shape (a diamond with width a multiple of 4), the light direction (upper left, so top, left, right are light, mid, dark on every block), and the character facing count (4 or 8). Stage 2 draws the diamond, the cube, and the character. Stage 4 places them with the formulas above, and the map cluster's generators supply the (tx,ty)(t_x, t_y) grid exactly as they do for top-down; only the screen mapping changes. Stage 5 exports each tile with its bounding box and pivot recorded, since a tile's drawn pixels no longer fill its rectangle.

Common mistakes

  • A ground edge with a kink in it. A 1:1 or 3:1 run crept into a 2:1 line. Redraw the edge as strict pairs.
  • Hairline gaps or doubled edges between tiles. The tile width is odd or not a multiple of 4, or the diamond has two rows of full width. Use 16, 32, or 64, and the 4, 8, 12, 16, 12, 8, 4 row pattern.
  • A character pops in front of a wall it is behind. Sprites are sorted by screen yy instead of by tx+tyt_x + t_y, or a sprite is sorted by its own pixel position rather than the tile it stands on.
  • Blocks look inside-out or lit from two directions. The left and right faces have the wrong values, or one block uses a different ramp order from its neighbors. Top lightest, left mid, right darkest, every time.
  • Round things look like diamonds. A circle on the ground was drawn as a 2:1 diamond instead of a 2:1 ellipse.
  • Characters drawn in true isometric. They read as tilted. Draw them upright and let the facing set do the work.

Cost

Isometric is the most expensive view per asset. Every block shows three faces, so a tile that would be one drawing in top-down is three shaded regions that must agree with every neighbor. Every character needs 4 or 8 facings, each with its own walk cycle, where a side-scroller needs one facing and a flip. A 32x16 tileset with cubes, slopes, and a few props is a few dozen drawings; a character with 8 facings and a 6-frame walk is 48 frames before idle and attack. Screen space costs too: a 3x3 map of 32x16 tiles is 96 by 48 pixels, and a map of NN by NN tiles is N⋅wtN \cdot w_t wide, so isometric shows fewer cells per screen than top-down at the same tile size. Placement and sorting are cheap by comparison: one multiply and add per tile, and one sort per change.

Going further

  • Shading and lighting, for the ramp the three faces are drawn from.
  • Character design for sprites, for the upright characters that stand on the tiles.
  • Tileable textures and tilesets, for making the top face texture repeat across the diamond grid.
  • Sprite sheets and export, for recording the bounding box and pivot of a diamond tile.

Back to Pixel art for games