DevilutionX research notes

How Diablo drew its sprite shadows

The shadow under every Diablo character is part of its sprite, stored as black pixels. Each shadow is a copy of the sprite's own outline, squashed to a third of its height and sheared to the left, so a few lines of integer arithmetic can recompute it from the rest of the frame. The same function works for players, monsters, townspeople and dungeon objects, in both Diablo and the Hellfire expansion.

Diablo's walk animation as stored in the game data. The black shape at his feet is part of each frame.
139,943
frames checked, from 1,438 sprite files in Diablo and Hellfire
99.77%
of the 136,936 frames with a shadow are reproduced pixel for pixel
763 of 765
original player files in which every frame is reproduced exactly
30,744 of 30,744
frames of the Hellfire monk reproduced exactly
3
rules for placing the ground line, which is the function's only input besides the sprite

The shadows were added in 2D

Diablo's characters and monsters were modelled and animated in 3D, then rendered to small 2D frames, one set per facing direction. The game ships only those frames. Each one is a grid of palette indices, and its shadow is part of it, as a patch of pure black pixels next to the feet.

These shadows did not come from the 3D renderer. A rendered shadow would depend on the depth of the model, and the ones in the game files can be computed from the flat frame alone. Each is an affine transform of the 2D sprite: the silhouette is sheared to the left, squashed to a third of its height and filled with black. Done in integer arithmetic, with the rounding described in the next section, the transform matches the game's art to the pixel.

It is applied to every pixel that is not transparent, including black pixels of the body such as dark boots, so it must have run on the finished 2D image.

The same animation with the shadow removed. The function recomputes the shadow from these pixels alone.

The function

The function takes the sprite and one number: B, the row of the ground line that the shadow is measured from. Rows are numbered downwards from the top of the frame.

// B: the row of the ground line, chosen by one of the rules further down.
// Called for every pixel (x, y) of the shadowless sprite with y <= B.
// Pixels below B cast nothing. Both divisions are integer divisions.
int v  = (B - y) / 3;        // three sprite rows collapse into one shadow row
int ys = B - 2 - v;          // the shadow starts two rows above the ground line
int xs = x - (3 * v) / 4;    // sheared left by 3/4 px per shadow row
shadow[ys][xs] = BLACK;      // the sprite is then drawn on top

In matrix form, with h = B - y for the height of a pixel above the ground line, this is a shear combined with a vertical squash:

[ x' ]   [ 1  -1/4 ] [ x ]
[ h' ] = [ 0   1/3 ] [ h ]        y' = B - 2 - h'

A pixel moves left by a quarter of its height and keeps a third of its height. The matrix only approximates the code, which rounds twice: it first reduces the height to a whole shadow row and then computes the shift from that row. Rounding the matrix product once puts some pixels in a different column, up to 1.25 pixels away. Only the two-step integer version matches the game's art exactly.

Try it on the original sprites

This page runs the function in your browser. For each original frame it removes the shadow, casts a new one from what is left, and compares the result with the original.

OriginalAs stored in the game data
Shadow removedThe frame as it was before the shadow pass
ReproducedShadow cast by the function, sprite drawn on top
Where each black pixel comes from 
DirectionFramesGround line ruleRow BBlack pixelsReproduced shadowBody blackUnexplainedStrayExact frames

Five sprites are in the list because the function does not fully explain them: the warrior with only a shield, standing and walking in town, Wirt, Na-Krul's death animation, and the cauldron. They are covered under What is not reproduced. Every sprite is drawn with the town palette, so the colours of dungeon monsters may be slightly off.

How the ground line is chosen

The ground line is always derived from the art, in one of three ways, and the choice differs from file to file. The table counts animation directions of the original game's monsters.

Ground line row BMonster directionsUsed by
Lowest opaque row of the first frame1,223all original players, most monsters, townspeople, barrels, levers, sarcophagi
Lowest opaque row of each frame75golem, some Diablo, mage, snake and mega demon animations, chests, books, nearly all Hellfire art
First frame's lowest row plus a constant72flying monsters: bat +15 rows, gargoyle +11
No shadow in the art80two monsters with no black pixels at all

Under the first-frame rule the shadow stays where it is while the body moves. Under the each-frame rule it follows a body that bobs up and down, as the golem's does.

For flying monsters the ground line lies below the sprite, which separates the shadow from the body. The offsets of 15 and 11 rows make more sense when measured from the body. The ground line is exactly 40 rows below the lowest non-black row of the first frame for the bat, in all 32 directions checked, and 28 rows below it for the gargoyle, in all 40. In those first frames the lowest opaque row is the bottom of the shadow, not of the body, which is why the offset from it comes out as +15 and +11.

We found no property of a sprite that predicts which rule its file uses. Animations of the same monster can use different rules, and no single rule covers both the first-frame and the each-frame files. Our guess is that the choice reflects how each file was processed at the time.

Hellfire

Hellfire was made by a different studio a year later, and it uses the same function. Its ground line is almost always the lowest opaque row of each frame, where the original game mostly uses the first frame.

Results by sprite set

Each row is one set of sprite files, measured with the ground line rule that the DevilutionX data files record for each file. The rule column counts files. The table above counts monster animation directions, so the two do not match.

Sprite setFilesFramesFiles with a shadowFirst / each / first + offsetFiles with no shadowFrames with a shadow reproduced exactlyStray pixelsUnexplained pixels
Players, original game76578,016765765 / 0 / 0077,872 of 78,016 (99.82%)7711,333
Monk, Hellfire29130,7442910 / 291 / 0030,744 of 30,744 (100%)00
Monsters, original game18318,088173151 / 13 / 91016,912 of 16,912 (100%)00
Monsters, Hellfire12512,4241141 / 113 / 01110,717 of 10,872 (98.57%)04,815
Townspeople, original game916088 / 0 / 01132 of 152 (86.84%)380
Townspeople, Hellfire35033 / 0 / 0050 of 50 (100%)00
Objects, original game533962815 / 13 / 025161 of 161 (100%)00
Objects, Hellfire96531 / 2 / 0629 of 29 (100%)00
All sprite sets1,438139,9431,385944 / 432 / 953136,617 of 136,936 (99.77%)8096,148
  • A frame is reproduced exactly when removing the shadow, casting a new one with the function and drawing the body on top gives back the original frame, pixel for pixel. Only frames of files with a shadow are counted.
  • Stray pixels are transparent pixels that the function casts a shadow onto. Each pixel is counted once.
  • Unexplained pixels are black pixels that are neither reproduced shadow nor body. Nothing left in the sprite casts them, and if they were body pixels they would cast a stray shadow themselves.
  • The rule for each file is the one recorded in the DevilutionX data files. A file recorded as having no shadow is counted that way even if it has black pixels. This applies to 13 files: five Hellfire death animations, six original objects and two Hellfire objects.
  • The Hellfire rows count only the files found in the Hellfire archives. The Hellfire object row uses the same object list as the original game.

What is not reproduced

Of the 136,936 frames with a shadow, 319 are not reproduced exactly. They fall into four groups. A fifth case, the cauldron, is reproduced exactly but cannot be split cleanly into body and shadow. The sprites on the right are drawn as stored, with unexplained pixels marked in pink and stray pixels in red.

The warrior with only a shield, standing in town in heavy armour. This file leaves 771 stray and 1,318 unexplained pixels over its eight directions, and we have not found the reason. Five directions fit best with the normal ground line and three with one a row higher, but no direction is exact. The errors are on the edges of the shield's shadow. We tried and ruled out a different ground line per frame, a shifted shadow, casting from another frame or another weapon variant, a separately shadowed shield layer with its own ground line or position, and two ground lines split by row. Our best guess is that the shield was redrawn after the shadow pass, but we have no direct evidence for it.

Facing north-east, the direction with the most errors.

The same warrior walking in light armour. One direction leaves 15 unexplained pixels, two or three per frame on the upper left edge of the shadow, in seven of its eight frames.

Facing north-west, the only direction with errors.

Wirt. He fits the first-frame rule except for 38 stray pixels over his 20 frames. They are cast by 44 pixels on the edge of his outline, one of them next to the peg leg in every frame. No other ground line does better. Those edge pixels were probably added after the shadow pass, though again we have no direct evidence.

Wirt has a single view. Every frame has one to four stray pixels.

Four Hellfire monster files. They have no stray pixels but leave 4,815 unexplained black pixels, 4,787 of them in Na-Krul's death and special animations. We have not investigated these yet.

Na-Krul's death animation, facing south.

The cauldron. All three frames count as reproduced exactly, but the split into body and shadow is wrong. The pot is nearly black: 664 to 802 of its pixels per frame are pure black, and 365 of them lie where the pixels above them cast a shadow. A black body pixel in that position cannot be told apart from shadow, so the function takes the lower part of the pot for shadow, and removing the shadow removes it too.

The cauldron with the shadow removed. The gaps in its base are body pixels that were taken for shadow.

Some sprites have black pixels and no cast shadow at all. In the original game these are two door types, the bookcase, the trap hole and the mushroom patch. Their black pixels are painted cracks, outlines and holes, and the area where a shadow would fall is empty. The fountain of tears has eight frames: the four that the game plays have no black pixels, and the other four carry a shadow that the function reproduces exactly. Hellfire's two pod objects have four black pixels each and no shadow.

How it was found