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.
- 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 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.
- shadow, reproduced
- black pixel of the body
- unexplained: black pixel that nothing in the sprite casts
- stray: shadow cast onto a transparent pixel
- rest of the body
| 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.
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 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
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.
- The monk has 291 files and 30,744 frames. With the each-frame rule every frame is reproduced pixel for pixel. The first-frame rule works only where the bottom row never moves, which is the case in 536 of 2,328 directions.
- Of the 125 Hellfire monster files checked, 114 have a shadow. All 114 fit the each-frame rule with no stray pixels, and none needs the first-frame rule. Four of them leave unexplained black pixels, 4,815 in total, nearly all in two Na-Krul animations.
- The other 11 are death animations with no cast shadow. Six have no black pixels at all. In the other five the few black pixels are painted detail: Hork Spawn, Hell Bug, Hellbat, Arch Lich and Hellfire's Satyr Lord. Black makes up at most 0.5% of their pixels, against 15% or more in death animations that have a shadow.
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 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 |
- 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.
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.
How it was found
- The starting point was Joan Karadimov's description of the transform and an approximate matrix for it, with a height scale near 1/3 and a shear near 0.26, posted in a comment on DevilutionX issue #8718. Fitting a single transform, in several number formats, got close but never exact.
- The next pass assumed no formula. For each sprite row it searched for the shadow row and horizontal shift that never put a shadow pixel on a transparent one. Across all frames exactly one combination survived per row, and together they form the table that the function above generates.
- The ground line was first taken to be the lowest non-black row, which fits the shareware warrior. The rogue's heavy armour has black boots, and for it only the lowest opaque row works.
- A black body pixel that lies inside the shadow of the pixels above it cannot be told apart from shadow. Where such a pixel is in the right place to cast an otherwise unexplained shadow pixel, the page counts it as body. In the original player sprites that leaves 1,333 black pixels unexplained, out of 23 million.
- Monsters and objects were classified by the rule under which their non-black pixels cast no stray shadow. Objects are weaker evidence because most have only a few frames. Of 34 original objects with black pixels, 15 fit the first-frame rule and 13 the each-frame rule.