mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-10-12 02:13:06 +00:00
The original sprites have their shadow drawn into them as black pixels.
All of these shadows were made by one function: the sprite squashed to
a third of its height and sheared to the left, measured from a baseline
row B:
v = (B - y) / 3
ys = B - 2 - v
xs = x - (3 * v) / 4
Only the choice of baseline differs between sprites. It is now recorded
per sprite in a new `shadowRule` column of the monster, object, towner
and class sprite data files:
* `FirstFrame`: the lowest opaque row of the first frame
* `EachFrame`: the lowest opaque row of each frame
* `FirstFrame+N`: the same plus N rows, for flying monsters
* `None`: no shadow
* `Baked`: a shadow that cannot be told apart from black parts of the
sprite, so it is left in the sprite
A rule can be followed by `|Unexplained` and `|Stray` for the few
sprites that the function does not reproduce exactly. The rule is empty
for the Hellfire bard and barbarian, whose sprites were not available.
When sprites are loaded, `ExtractClxShadows` removes the shadow from
each sprite and stores it as a mask of horizontal spans in the CLX
frame header, so it is available wherever the sprite is. The shadow is
drawn right before its sprite, which gives the same picture as before.
A new "Transparent Shadows" graphics option blends the shadows with
what is underneath instead of covering it. It is off by default.
In debug builds, `dev.display.shadows()` toggles drawing them.
Transparent shadows are not perfect. A black pixel of a sprite that
lies inside the sprite's own shadow looks exactly like shadow, so it is
extracted with it and becomes see-through. For example, the warrior in
light armor with a sword and shield, facing southwest, is missing a few
pixels from his left knee. Getting this perfect will likely require
overriding the shadow masks of a handful of sprites, which is something
that can be done with AI in the future.
The rules were found and checked with `tools/shadow_search`, against
all player, monster, towner and object sprites of Diablo and Hellfire:
139,943 frames in 1,438 files. Of the 136,918 frames that have an
extractable shadow, 99.77% are reproduced pixel for pixel.
Extraction adds about 14 us per frame when a sprite is loaded
(about 14 ms for all animations of a character). For the original
player sprites, the sprites with their shadow masks take 0.3% less
memory than before.
Drawing a sprite with its shadow, per sprite, for all sprites of each
kind (`shadow_extract_check <mpq> render`, release build, fully visible
sprites, Ryzen AI Max+ 395):
Players Monsters Monk
Without lighting
Before 2,111 ns 2,444 ns 2,109 ns
Opaque shadow -9.5% -10.1% -6.1%
Transparent shadow +10.0% +7.5% +9.0%
With a light table
Before 2,659 ns 2,983 ns 2,548 ns
Opaque shadow -5.2% -4.5% -2.3%
Transparent shadow +10.9% +10.2% +10.4%
Opaque shadows are a little faster to draw than before: a shadow is now
a short list of spans instead of many commands in the sprite. Blending
a shadow costs about as much again as that saves. Either way the frame
rate is practically unchanged: with about 10 such sprites per frame,
it comes to about +0.2% FPS with opaque shadows and -0.3% with
transparent ones in an unthrottled demo playback, where the 3 modes
measured within noise of each other.
Selection outlines can differ by a pixel where a shadow touches the
sprite: their shape used to depend on how the black pixels of the
shadow were encoded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
143 lines
4.0 KiB
C++
143 lines
4.0 KiB
C++
#ifdef _DEBUG
|
|
#include "lua/modules/dev/display.hpp"
|
|
|
|
#include <array>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
#include <sol/sol.hpp>
|
|
|
|
#include "debug.h"
|
|
#include "lighting.h"
|
|
#include "lua/metadoc.hpp"
|
|
#include "player.h"
|
|
#include "utils/str_cat.hpp"
|
|
|
|
namespace devilution {
|
|
namespace {
|
|
|
|
std::string DebugCmdShowGrid(std::optional<bool> on)
|
|
{
|
|
DebugGrid = on.value_or(!DebugGrid);
|
|
return StrCat("Tile grid highlighting: ", DebugGrid ? "On" : "Off");
|
|
}
|
|
|
|
std::string DebugCmdVision(std::optional<bool> on)
|
|
{
|
|
DebugVision = on.value_or(!DebugVision);
|
|
return StrCat("Vision highlighting: ", DebugVision ? "On" : "Off");
|
|
}
|
|
|
|
std::string DebugCmdPath(std::optional<bool> on)
|
|
{
|
|
DebugPath = on.value_or(!DebugPath);
|
|
return StrCat("Path highlighting: ", DebugPath ? "On" : "Off");
|
|
}
|
|
|
|
std::string DebugCmdShadows(std::optional<bool> on)
|
|
{
|
|
DebugHideShadows = !on.value_or(DebugHideShadows);
|
|
return StrCat("Sprite shadows: ", DebugHideShadows ? "Off" : "On");
|
|
}
|
|
|
|
std::string DebugCmdFullbright(std::optional<bool> on)
|
|
{
|
|
ToggleLighting();
|
|
return StrCat("Fullbright: ", DisableLighting ? "On" : "Off");
|
|
}
|
|
|
|
std::string DebugCmdShowTileData(std::optional<std::string_view> dataType)
|
|
{
|
|
static const std::array<std::string_view, 23> DataTypes {
|
|
"microTiles",
|
|
"dPiece",
|
|
"dTransVal",
|
|
"dLight",
|
|
"dPreLight",
|
|
"dFlags",
|
|
"dPlayer",
|
|
"dMonster",
|
|
"missiles",
|
|
"dCorpse",
|
|
"dObject",
|
|
"dItem",
|
|
"dSpecial",
|
|
"coords",
|
|
"cursorcoords",
|
|
"objectindex",
|
|
"solid",
|
|
"transparent",
|
|
"trap",
|
|
"AutomapView",
|
|
"dungeon",
|
|
"pdungeon",
|
|
"Protected",
|
|
};
|
|
if (!dataType.has_value()) {
|
|
std::string result = "Valid values for the first argument:\nclear";
|
|
for (const std::string_view &str : DataTypes)
|
|
StrAppend(result, ", ", str);
|
|
return result;
|
|
}
|
|
if (*dataType == "clear") {
|
|
SetDebugGridTextType(DebugGridTextItem::None);
|
|
return "Tile data cleared.";
|
|
}
|
|
bool found = false;
|
|
int index = 0;
|
|
for (const std::string_view ¶m : DataTypes) {
|
|
index++;
|
|
if (*dataType != param)
|
|
continue;
|
|
found = true;
|
|
auto newGridText = static_cast<DebugGridTextItem>(index);
|
|
if (newGridText == GetDebugGridTextType()) {
|
|
SetDebugGridTextType(DebugGridTextItem::None);
|
|
return "Tile data: Off";
|
|
}
|
|
SetDebugGridTextType(newGridText);
|
|
break;
|
|
}
|
|
if (!found) {
|
|
std::string result = "Invalid name! Valid names are:\nclear";
|
|
for (const std::string_view &str : DataTypes)
|
|
StrAppend(result, ", ", str);
|
|
return result;
|
|
}
|
|
|
|
return "Tile data: On";
|
|
}
|
|
|
|
std::string DebugCmdScrollView(std::optional<bool> on)
|
|
{
|
|
DebugScrollViewEnabled = on.value_or(!DebugScrollViewEnabled);
|
|
if (!DebugScrollViewEnabled)
|
|
InitMultiView();
|
|
return StrCat("Scroll view: ", DebugScrollViewEnabled ? "On" : "Off");
|
|
}
|
|
|
|
std::string DebugCmdToggleFPS(std::optional<bool> on)
|
|
{
|
|
frameflag = on.value_or(!frameflag);
|
|
return StrCat("FPS counter: ", frameflag ? "On" : "Off");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
sol::table LuaDevDisplayModule(sol::state_view &lua)
|
|
{
|
|
sol::table table = lua.create_table();
|
|
LuaSetDocFn(table, "fps", "(name: string = nil)", "Toggle FPS display.", &DebugCmdToggleFPS);
|
|
LuaSetDocFn(table, "fullbright", "(on: boolean = nil)", "Toggle light shading.", &DebugCmdFullbright);
|
|
LuaSetDocFn(table, "grid", "(on: boolean = nil)", "Toggle showing the grid.", &DebugCmdShowGrid);
|
|
LuaSetDocFn(table, "path", "(on: boolean = nil)", "Toggle path debug rendering.", &DebugCmdPath);
|
|
LuaSetDocFn(table, "scrollView", "(on: boolean = nil)", "Toggle view scrolling via Shift+Mouse.", &DebugCmdScrollView);
|
|
LuaSetDocFn(table, "shadows", "(on: boolean = nil)", "Toggle drawing the shadows of sprites. Shadows that are still part of their sprite stay visible.", &DebugCmdShadows);
|
|
LuaSetDocFn(table, "tileData", "(name: string = nil)", "Toggle showing tile data.", &DebugCmdShowTileData);
|
|
LuaSetDocFn(table, "vision", "(on: boolean = nil)", "Toggle vision debug rendering.", &DebugCmdVision);
|
|
return table;
|
|
}
|
|
|
|
} // namespace devilution
|
|
#endif // _DEBUG
|