DevilutionX research notes
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 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 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.
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.
The sprites could not be unpacked. This page needs a browser with DecompressionStream, which all current browsers have.
| Direction | Frames | Ground line rule | Row B | Black pixels | Reproduced shadow | Body black | Unexplained | Stray | Exact 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.
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 B | Monster directions | Used by |
|---|---|---|
| Lowest opaque row of the first frame | 1,223 | all original players, most monsters, townspeople, barrels, levers, sarcophagi |
| Lowest opaque row of each frame | 75 | golem, some Diablo, mage, snake and mega demon animations, chests, books, nearly all Hellfire art |
| First frame's lowest row plus a constant | 72 | flying monsters: bat +15 rows, gargoyle +11 |
| No shadow in the art | 80 | two 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 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.
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 set | Files | Frames | Files with a shadow | First / each / first + offset | Files with no shadow | Frames with a shadow reproduced exactly | Stray pixels | Unexplained pixels |
|---|---|---|---|---|---|---|---|---|
| Players, original game | 765 | 78,016 | 765 | 765 / 0 / 0 | 0 | 77,872 of 78,016 (99.82%) | 771 | 1,333 |
| Monk, Hellfire | 291 | 30,744 | 291 | 0 / 291 / 0 | 0 | 30,744 of 30,744 (100%) | 0 | 0 |
| Monsters, original game | 183 | 18,088 | 173 | 151 / 13 / 9 | 10 | 16,912 of 16,912 (100%) | 0 | 0 |
| Monsters, Hellfire | 125 | 12,424 | 114 | 1 / 113 / 0 | 11 | 10,717 of 10,872 (98.57%) | 0 | 4,815 |
| Townspeople, original game | 9 | 160 | 8 | 8 / 0 / 0 | 1 | 132 of 152 (86.84%) | 38 | 0 |
| Townspeople, Hellfire | 3 | 50 | 3 | 3 / 0 / 0 | 0 | 50 of 50 (100%) | 0 | 0 |
| Objects, original game | 53 | 396 | 28 | 15 / 13 / 0 | 25 | 161 of 161 (100%) | 0 | 0 |
| Objects, Hellfire | 9 | 65 | 3 | 1 / 2 / 0 | 6 | 29 of 29 (100%) | 0 | 0 |
| All sprite sets | 1,438 | 139,943 | 1,385 | 944 / 432 / 9 | 53 | 136,617 of 136,936 (99.77%) | 809 | 6,148 |
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.
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.
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.
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.
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.
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.