You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

698 lines
23 KiB

/**
* @file lighting.cpp
*
* Implementation of light and vision.
*/
#include "lighting.h"
#include <algorithm>
#include <cstdint>
#include <numeric>
#include <string>
#include <expected.hpp>
#include "automap.h"
#include "engine/load_file.hpp"
#include "engine/points_in_rectangle_range.hpp"
#include "player.h"
#include "utils/attributes.h"
#include "utils/is_of.hpp"
#include "utils/status_macros.hpp"
namespace devilution {
std::array<bool, MAXVISION> VisionActive;
Light VisionList[MAXVISION];
Light Lights[MAXLIGHTS];
std::array<uint8_t, MAXLIGHTS> ActiveLights;
int ActiveLightCount;
std::array<std::array<uint8_t, 256>, NumLightingLevels> LightTables;
uint8_t *FullyLitLightTable = nullptr;
uint8_t *FullyDarkLightTable = nullptr;
std::array<uint8_t, 256> InfravisionTable;
std::array<uint8_t, 256> StoneTable;
std::array<uint8_t, 256> PauseTable;
#ifdef _DEBUG
bool DisableLighting;
#endif
bool UpdateLighting;
namespace {
/*
lighting: fix long-standing issue with invisible objects (#7901) * lighting: fix long-standing issue with invisible objects This fixes an issue with lighting, where objects in a straight line of sight parallel to the X or Y coordinate lines become invisible. Issue #6641 perfectly illustrates the bug (see video attached to the bug). What's worse, the objects are invisible to the observer (player) regardless of the distance to that objects. The main requirement of a bug reproduction is line of sight parallel to the X or Y coordinate lines. The actual bug lies in the visibility checks of adjacent tiles of a point, hit by the cast ray. We've cast an approximated ray on an integer 2D grid, so we need to check if a ray can pass through the diagonally adjacent tiles. For example, consider this case: #? ↗ # x The ray is cast from the observer 'x', and reaches the '?', but diagonally adjacent tiles '#' do not pass the light, so the '?' should not be visible for the 2D observer. The trick is to perform two additional visibility checks for the diagonally adjacent tiles, but only for the rays that are not parallel to the X or Y coordinate lines. Parallel rays, which have a 0 in one of their coordinate components, do not require any additional adjacent visibility checks, and the tile, hit by the ray, is always considered visible. For the rays that parallel to the X or Y coordinate lines, the adjacent visibility check always degenerated to the actual ray point visibility check, which is considered invisible if it does not allow light to pass through, and this is the actual bug. To fix the issue, ensure the tile is always considered visible if the ray that hits it is parallel to the X or Y coordinate lines. To better demonstrate the problem, here's a straightforward simulation written in Python: https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5 The code simulates lighting from the DevilitionX implementation, by placing the observer 'x' in the center of the grid. The observer is surrounded by walls and 5 random obstacles, '.' are marked as visible, were hit by the cast rays. The first matrix output shows the bug: no walls and obstacles are visible in the line of sight parallel to the X and Y coordinate lines. In contrast, the second matrix output (with the fix applied) does not exhibit this problem. Also, note the box corners are not visible due to the adjacent visibility checks, which are functioning correctly. Fixes: #6641 Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * lighting: rename variables and add explicit comments for clarity This patch improves clarity and readability without affecting functionality: 1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to `rayPoint` for better clarity on the purpose of these structures. 2. Renames `factors` to `quadrants` to reflect the actual purpose of the mirror operation along the X or Y coordinate lines. 3. Adds more explicit comments to simplify the understanding of the ray casting algorithm. Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * test/WarriorLevel1to2: update timedemo Recent visibility fix impacts game state and causes the timedemo to behave completely differently, resulting in a butterfly effect: https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which was recorded with the visibility fixes applied, ensuring the tests pass successfully. Here's the latest timedemo video for the future generations: https://youtu.be/udGcWmarYNI Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> --------- Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
* XY points of vision rays are cast to trace the visibility of the
* surrounding environment. The table represents N rays of M points in
* one quadrant (0°-90°) of a circle, so rays for other quadrants will
* be created by mirroring. Zero points at the end will be trimmed and
* ignored. A similar table can be recreated using Bresenham's line
* drawing algorithm, which is suitable for integer arithmetic:
* https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
*/
lighting: fix long-standing issue with invisible objects (#7901) * lighting: fix long-standing issue with invisible objects This fixes an issue with lighting, where objects in a straight line of sight parallel to the X or Y coordinate lines become invisible. Issue #6641 perfectly illustrates the bug (see video attached to the bug). What's worse, the objects are invisible to the observer (player) regardless of the distance to that objects. The main requirement of a bug reproduction is line of sight parallel to the X or Y coordinate lines. The actual bug lies in the visibility checks of adjacent tiles of a point, hit by the cast ray. We've cast an approximated ray on an integer 2D grid, so we need to check if a ray can pass through the diagonally adjacent tiles. For example, consider this case: #? ↗ # x The ray is cast from the observer 'x', and reaches the '?', but diagonally adjacent tiles '#' do not pass the light, so the '?' should not be visible for the 2D observer. The trick is to perform two additional visibility checks for the diagonally adjacent tiles, but only for the rays that are not parallel to the X or Y coordinate lines. Parallel rays, which have a 0 in one of their coordinate components, do not require any additional adjacent visibility checks, and the tile, hit by the ray, is always considered visible. For the rays that parallel to the X or Y coordinate lines, the adjacent visibility check always degenerated to the actual ray point visibility check, which is considered invisible if it does not allow light to pass through, and this is the actual bug. To fix the issue, ensure the tile is always considered visible if the ray that hits it is parallel to the X or Y coordinate lines. To better demonstrate the problem, here's a straightforward simulation written in Python: https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5 The code simulates lighting from the DevilitionX implementation, by placing the observer 'x' in the center of the grid. The observer is surrounded by walls and 5 random obstacles, '.' are marked as visible, were hit by the cast rays. The first matrix output shows the bug: no walls and obstacles are visible in the line of sight parallel to the X and Y coordinate lines. In contrast, the second matrix output (with the fix applied) does not exhibit this problem. Also, note the box corners are not visible due to the adjacent visibility checks, which are functioning correctly. Fixes: #6641 Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * lighting: rename variables and add explicit comments for clarity This patch improves clarity and readability without affecting functionality: 1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to `rayPoint` for better clarity on the purpose of these structures. 2. Renames `factors` to `quadrants` to reflect the actual purpose of the mirror operation along the X or Y coordinate lines. 3. Adds more explicit comments to simplify the understanding of the ray casting algorithm. Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * test/WarriorLevel1to2: update timedemo Recent visibility fix impacts game state and causes the timedemo to behave completely differently, resulting in a butterfly effect: https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which was recorded with the visibility fixes applied, ensuring the tests pass successfully. Here's the latest timedemo video for the future generations: https://youtu.be/udGcWmarYNI Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> --------- Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
static const DisplacementOf<int8_t> VisionRays[23][15] = {
// clang-format off
{ { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 0 }, { 5, 0 }, { 6, 0 }, { 7, 0 }, { 8, 0 }, { 9, 0 }, { 10, 0 }, { 11, 0 }, { 12, 0 }, { 13, 0 }, { 14, 0 }, { 15, 0 } },
{ { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 0 }, { 5, 0 }, { 6, 0 }, { 7, 0 }, { 8, 1 }, { 9, 1 }, { 10, 1 }, { 11, 1 }, { 12, 1 }, { 13, 1 }, { 14, 1 }, { 15, 1 } },
{ { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 1 }, { 9, 1 }, { 10, 1 }, { 11, 1 }, { 12, 2 }, { 13, 2 }, { 14, 2 }, { 15, 2 } },
{ { 1, 0 }, { 2, 0 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 2 }, { 9, 2 }, { 10, 2 }, { 11, 2 }, { 12, 2 }, { 13, 3 }, { 14, 3 }, { 15, 3 } },
{ { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 2 }, { 7, 2 }, { 8, 2 }, { 9, 3 }, { 10, 3 }, { 11, 3 }, { 12, 3 }, { 13, 4 }, { 14, 4 }, { 0, 0 } },
{ { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 2 }, { 6, 2 }, { 7, 3 }, { 8, 3 }, { 9, 3 }, { 10, 4 }, { 11, 4 }, { 12, 4 }, { 13, 5 }, { 14, 5 }, { 0, 0 } },
{ { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 2 }, { 5, 2 }, { 6, 3 }, { 7, 3 }, { 8, 3 }, { 9, 4 }, { 10, 4 }, { 11, 5 }, { 12, 5 }, { 13, 6 }, { 14, 6 }, { 0, 0 } },
{ { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 2 }, { 5, 3 }, { 6, 3 }, { 7, 4 }, { 8, 4 }, { 9, 5 }, { 10, 5 }, { 11, 6 }, { 12, 6 }, { 13, 7 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 2 }, { 5, 3 }, { 6, 4 }, { 7, 4 }, { 8, 5 }, { 9, 6 }, { 10, 6 }, { 11, 7 }, { 12, 7 }, { 12, 8 }, { 13, 8 }, { 0, 0 } },
{ { 1, 1 }, { 2, 2 }, { 3, 2 }, { 4, 3 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 7 }, { 10, 8 }, { 11, 8 }, { 12, 9 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 6, 5 }, { 7, 6 }, { 8, 7 }, { 9, 8 }, { 10, 9 }, { 11, 9 }, { 11, 10 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 6, 6 }, { 7, 7 }, { 8, 8 }, { 9, 9 }, { 10, 10 }, { 11, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 5, 6 }, { 6, 7 }, { 7, 8 }, { 8, 9 }, { 9, 10 }, { 9, 11 }, { 10, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 2, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 }, { 5, 7 }, { 6, 8 }, { 7, 9 }, { 7, 10 }, { 8, 10 }, { 8, 11 }, { 9, 12 }, { 0, 0 }, { 0, 0 } },
{ { 1, 1 }, { 1, 2 }, { 2, 3 }, { 2, 4 }, { 3, 5 }, { 4, 6 }, { 4, 7 }, { 5, 8 }, { 6, 9 }, { 6, 10 }, { 7, 11 }, { 7, 12 }, { 8, 12 }, { 8, 13 }, { 0, 0 } },
{ { 1, 1 }, { 1, 2 }, { 2, 3 }, { 2, 4 }, { 3, 5 }, { 3, 6 }, { 4, 7 }, { 4, 8 }, { 5, 9 }, { 5, 10 }, { 6, 11 }, { 6, 12 }, { 7, 13 }, { 0, 0 }, { 0, 0 } },
{ { 0, 1 }, { 1, 2 }, { 1, 3 }, { 2, 4 }, { 2, 5 }, { 3, 6 }, { 3, 7 }, { 3, 8 }, { 4, 9 }, { 4, 10 }, { 5, 11 }, { 5, 12 }, { 6, 13 }, { 6, 14 }, { 0, 0 } },
{ { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 2, 5 }, { 2, 6 }, { 3, 7 }, { 3, 8 }, { 3, 9 }, { 4, 10 }, { 4, 11 }, { 4, 12 }, { 5, 13 }, { 5, 14 }, { 0, 0 } },
{ { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 2, 6 }, { 2, 7 }, { 2, 8 }, { 3, 9 }, { 3, 10 }, { 3, 11 }, { 3, 12 }, { 4, 13 }, { 4, 14 }, { 0, 0 } },
{ { 0, 1 }, { 0, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 7 }, { 2, 8 }, { 2, 9 }, { 2, 10 }, { 2, 11 }, { 2, 12 }, { 3, 13 }, { 3, 14 }, { 3, 15 } },
{ { 0, 1 }, { 0, 2 }, { 0, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 7 }, { 1, 8 }, { 1, 9 }, { 1, 10 }, { 1, 11 }, { 2, 12 }, { 2, 13 }, { 2, 14 }, { 2, 15 } },
{ { 0, 1 }, { 0, 2 }, { 0, 3 }, { 0, 4 }, { 0, 5 }, { 0, 6 }, { 0, 7 }, { 1, 8 }, { 1, 9 }, { 1, 10 }, { 1, 11 }, { 1, 12 }, { 1, 13 }, { 1, 14 }, { 1, 15 } },
{ { 0, 1 }, { 0, 2 }, { 0, 3 }, { 0, 4 }, { 0, 5 }, { 0, 6 }, { 0, 7 }, { 0, 8 }, { 0, 9 }, { 0, 10 }, { 0, 11 }, { 0, 12 }, { 0, 13 }, { 0, 14 }, { 0, 15 } },
// clang-format on
};
/** @brief Number of supported light radiuses (first radius starts with 0) */
constexpr size_t NumLightRadiuses = 16;
/** Falloff tables for the light cone */
uint8_t LightFalloffs[NumLightRadiuses][128];
bool UpdateVision;
/** interpolations of a 32x32 (16x16 mirrored) light circle moving between tiles in steps of 1/8 of a tile */
uint8_t LightConeInterpolations[8][8][16][16];
void RotateRadius(DisplacementOf<int8_t> &offset, DisplacementOf<int8_t> &dist, DisplacementOf<int8_t> &light, DisplacementOf<int8_t> &block)
{
dist = { static_cast<int8_t>(7 - dist.deltaY), dist.deltaX };
light = { static_cast<int8_t>(7 - light.deltaY), light.deltaX };
offset = { static_cast<int8_t>(dist.deltaX - light.deltaX), static_cast<int8_t>(dist.deltaY - light.deltaY) };
block.deltaX = 0;
if (offset.deltaX < 0) {
offset.deltaX += 8;
block.deltaX = 1;
}
block.deltaY = 0;
if (offset.deltaY < 0) {
offset.deltaY += 8;
block.deltaY = 1;
}
}
DVL_ALWAYS_INLINE void SetLight(Point position, uint8_t v)
{
if (LoadingMapObjects)
dPreLight[position.x][position.y] = v;
else
dLight[position.x][position.y] = v;
}
DVL_ALWAYS_INLINE uint8_t GetLight(Point position)
{
if (LoadingMapObjects)
return dPreLight[position.x][position.y];
return dLight[position.x][position.y];
}
bool TileAllowsLight(Point position)
{
if (!InDungeonBounds(position))
return false;
return !TileHasAny(position, TileProperties::BlockLight);
}
void DoVisionFlags(Point position, MapExplorationType doAutomap, bool visible)
{
if (doAutomap != MAP_EXP_NONE) {
if (dFlags[position.x][position.y] != DungeonFlag::None)
SetAutomapView(position, doAutomap);
dFlags[position.x][position.y] |= DungeonFlag::Explored;
}
if (visible)
dFlags[position.x][position.y] |= DungeonFlag::Lit;
dFlags[position.x][position.y] |= DungeonFlag::Visible;
}
} // namespace
void DoUnLight(Point position, uint8_t radius)
{
radius++;
radius++; // If lights moved at a diagonal it can result in some extra tiles being lit
auto searchArea = PointsInRectangle(WorldTileRectangle { position, radius });
for (WorldTilePosition targetPosition : searchArea) {
if (InDungeonBounds(targetPosition))
dLight[targetPosition.x][targetPosition.y] = dPreLight[targetPosition.x][targetPosition.y];
}
}
void DoLighting(Point position, uint8_t radius, DisplacementOf<int8_t> offset)
{
assert(radius >= 0 && radius <= NumLightRadiuses);
assert(InDungeonBounds(position));
DisplacementOf<int8_t> light = {};
DisplacementOf<int8_t> block = {};
if (offset.deltaX < 0) {
offset.deltaX += 8;
position -= { 1, 0 };
}
if (offset.deltaY < 0) {
offset.deltaY += 8;
position -= { 0, 1 };
}
DisplacementOf<int8_t> dist = offset;
int minX = 15;
if (position.x - 15 < 0) {
minX = position.x + 1;
}
int maxX = 15;
if (position.x + 15 > MAXDUNX) {
maxX = MAXDUNX - position.x;
}
int minY = 15;
if (position.y - 15 < 0) {
minY = position.y + 1;
}
int maxY = 15;
if (position.y + 15 > MAXDUNY) {
maxY = MAXDUNY - position.y;
}
// Allow for dim lights in crypt and nest
if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) {
if (GetLight(position) > LightFalloffs[radius][0])
SetLight(position, LightFalloffs[radius][0]);
} else {
SetLight(position, 0);
}
for (int i = 0; i < 4; i++) {
int yBound = i > 0 && i < 3 ? maxY : minY;
int xBound = i < 2 ? maxX : minX;
for (int y = 0; y < yBound; y++) {
for (int x = 1; x < xBound; x++) {
int linearDistance = LightConeInterpolations[offset.deltaX][offset.deltaY][x + block.deltaX][y + block.deltaY];
if (linearDistance >= 128)
continue;
Point temp = position + (Displacement { x, y }).Rotate(-i);
uint8_t v = LightFalloffs[radius][linearDistance];
if (!InDungeonBounds(temp))
continue;
if (v < GetLight(temp))
SetLight(temp, v);
}
}
RotateRadius(offset, dist, light, block);
}
}
void DoUnVision(Point position, uint8_t radius)
{
radius++;
radius++; // increasing the radius even further here prevents leaving stray vision tiles behind and doesn't seem to affect monster AI - applying new vision happens in the same tick
auto searchArea = PointsInRectangle(WorldTileRectangle { position, radius });
for (WorldTilePosition targetPosition : searchArea) {
if (InDungeonBounds(targetPosition))
dFlags[targetPosition.x][targetPosition.y] &= ~(DungeonFlag::Visible | DungeonFlag::Lit);
}
}
void DoVision(Point position, uint8_t radius,
tl::function_ref<void(Point)> markVisibleFn,
tl::function_ref<void(Point)> markTransparentFn,
tl::function_ref<bool(Point)> passesLightFn,
tl::function_ref<bool(Point)> inBoundsFn)
{
markVisibleFn(position);
lighting: fix long-standing issue with invisible objects (#7901) * lighting: fix long-standing issue with invisible objects This fixes an issue with lighting, where objects in a straight line of sight parallel to the X or Y coordinate lines become invisible. Issue #6641 perfectly illustrates the bug (see video attached to the bug). What's worse, the objects are invisible to the observer (player) regardless of the distance to that objects. The main requirement of a bug reproduction is line of sight parallel to the X or Y coordinate lines. The actual bug lies in the visibility checks of adjacent tiles of a point, hit by the cast ray. We've cast an approximated ray on an integer 2D grid, so we need to check if a ray can pass through the diagonally adjacent tiles. For example, consider this case: #? ↗ # x The ray is cast from the observer 'x', and reaches the '?', but diagonally adjacent tiles '#' do not pass the light, so the '?' should not be visible for the 2D observer. The trick is to perform two additional visibility checks for the diagonally adjacent tiles, but only for the rays that are not parallel to the X or Y coordinate lines. Parallel rays, which have a 0 in one of their coordinate components, do not require any additional adjacent visibility checks, and the tile, hit by the ray, is always considered visible. For the rays that parallel to the X or Y coordinate lines, the adjacent visibility check always degenerated to the actual ray point visibility check, which is considered invisible if it does not allow light to pass through, and this is the actual bug. To fix the issue, ensure the tile is always considered visible if the ray that hits it is parallel to the X or Y coordinate lines. To better demonstrate the problem, here's a straightforward simulation written in Python: https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5 The code simulates lighting from the DevilitionX implementation, by placing the observer 'x' in the center of the grid. The observer is surrounded by walls and 5 random obstacles, '.' are marked as visible, were hit by the cast rays. The first matrix output shows the bug: no walls and obstacles are visible in the line of sight parallel to the X and Y coordinate lines. In contrast, the second matrix output (with the fix applied) does not exhibit this problem. Also, note the box corners are not visible due to the adjacent visibility checks, which are functioning correctly. Fixes: #6641 Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * lighting: rename variables and add explicit comments for clarity This patch improves clarity and readability without affecting functionality: 1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to `rayPoint` for better clarity on the purpose of these structures. 2. Renames `factors` to `quadrants` to reflect the actual purpose of the mirror operation along the X or Y coordinate lines. 3. Adds more explicit comments to simplify the understanding of the ray casting algorithm. Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * test/WarriorLevel1to2: update timedemo Recent visibility fix impacts game state and causes the timedemo to behave completely differently, resulting in a butterfly effect: https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which was recorded with the visibility fixes applied, ensuring the tests pass successfully. Here's the latest timedemo video for the future generations: https://youtu.be/udGcWmarYNI Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> --------- Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
// Adjustment to a ray length to ensure all rays lie on an
// accurate circle
static const uint8_t rayLenAdj[23] = { 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 4, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0 };
static_assert(std::size(rayLenAdj) == std::size(VisionRays));
// Four quadrants on a circle
static const Displacement quadrants[] = { { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 } };
// Loop over quadrants and mirror rays for each one
for (const auto &quadrant : quadrants) {
// Cast a ray for a quadrant
for (unsigned int j = 0; j < std::size(VisionRays); j++) {
int rayLen = radius - rayLenAdj[j];
for (int k = 0; k < rayLen; k++) {
const auto &relRayPoint = VisionRays[j][k];
// Calculate the next point on a ray in the quadrant
Point rayPoint = position + relRayPoint * quadrant;
if (!inBoundsFn(rayPoint))
break;
lighting: fix long-standing issue with invisible objects (#7901) * lighting: fix long-standing issue with invisible objects This fixes an issue with lighting, where objects in a straight line of sight parallel to the X or Y coordinate lines become invisible. Issue #6641 perfectly illustrates the bug (see video attached to the bug). What's worse, the objects are invisible to the observer (player) regardless of the distance to that objects. The main requirement of a bug reproduction is line of sight parallel to the X or Y coordinate lines. The actual bug lies in the visibility checks of adjacent tiles of a point, hit by the cast ray. We've cast an approximated ray on an integer 2D grid, so we need to check if a ray can pass through the diagonally adjacent tiles. For example, consider this case: #? ↗ # x The ray is cast from the observer 'x', and reaches the '?', but diagonally adjacent tiles '#' do not pass the light, so the '?' should not be visible for the 2D observer. The trick is to perform two additional visibility checks for the diagonally adjacent tiles, but only for the rays that are not parallel to the X or Y coordinate lines. Parallel rays, which have a 0 in one of their coordinate components, do not require any additional adjacent visibility checks, and the tile, hit by the ray, is always considered visible. For the rays that parallel to the X or Y coordinate lines, the adjacent visibility check always degenerated to the actual ray point visibility check, which is considered invisible if it does not allow light to pass through, and this is the actual bug. To fix the issue, ensure the tile is always considered visible if the ray that hits it is parallel to the X or Y coordinate lines. To better demonstrate the problem, here's a straightforward simulation written in Python: https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5 The code simulates lighting from the DevilitionX implementation, by placing the observer 'x' in the center of the grid. The observer is surrounded by walls and 5 random obstacles, '.' are marked as visible, were hit by the cast rays. The first matrix output shows the bug: no walls and obstacles are visible in the line of sight parallel to the X and Y coordinate lines. In contrast, the second matrix output (with the fix applied) does not exhibit this problem. Also, note the box corners are not visible due to the adjacent visibility checks, which are functioning correctly. Fixes: #6641 Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * lighting: rename variables and add explicit comments for clarity This patch improves clarity and readability without affecting functionality: 1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to `rayPoint` for better clarity on the purpose of these structures. 2. Renames `factors` to `quadrants` to reflect the actual purpose of the mirror operation along the X or Y coordinate lines. 3. Adds more explicit comments to simplify the understanding of the ray casting algorithm. Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * test/WarriorLevel1to2: update timedemo Recent visibility fix impacts game state and causes the timedemo to behave completely differently, resulting in a butterfly effect: https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which was recorded with the visibility fixes applied, ensuring the tests pass successfully. Here's the latest timedemo video for the future generations: https://youtu.be/udGcWmarYNI Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> --------- Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
// We've cast an approximated ray on an integer 2D
// grid, so we need to check if a ray can pass through
// the diagonally adjacent tiles. For example, consider
// this case:
//
// #?
// ↗ #
// x
//
// The ray is cast from the observer 'x', and reaches
// the '?', but diagonally adjacent tiles '#' do not
// pass the light, so the '?' should not be visible
// for the 2D observer.
//
// The trick is to perform two additional visibility
// checks for the diagonally adjacent tiles, but only
// for the rays that are not parallel to the X or Y
// coordinate lines. Parallel rays, which have a 0 in
// one of their coordinate components, do not require
// any additional adjacent visibility checks, and the
// tile, hit by the ray, is always considered visible.
//
if (relRayPoint.deltaX > 0 && relRayPoint.deltaY > 0) {
Displacement adjacent1 = { -quadrant.deltaX, 0 };
Displacement adjacent2 = { 0, -quadrant.deltaY };
bool passesLight = (passesLightFn(rayPoint + adjacent1) || passesLightFn(rayPoint + adjacent2));
if (!passesLight)
// Diagonally adjacent tiles do not pass the
// light further, we are done with this ray
break;
}
markVisibleFn(rayPoint);
bool passesLight = passesLightFn(rayPoint);
lighting: fix long-standing issue with invisible objects (#7901) * lighting: fix long-standing issue with invisible objects This fixes an issue with lighting, where objects in a straight line of sight parallel to the X or Y coordinate lines become invisible. Issue #6641 perfectly illustrates the bug (see video attached to the bug). What's worse, the objects are invisible to the observer (player) regardless of the distance to that objects. The main requirement of a bug reproduction is line of sight parallel to the X or Y coordinate lines. The actual bug lies in the visibility checks of adjacent tiles of a point, hit by the cast ray. We've cast an approximated ray on an integer 2D grid, so we need to check if a ray can pass through the diagonally adjacent tiles. For example, consider this case: #? ↗ # x The ray is cast from the observer 'x', and reaches the '?', but diagonally adjacent tiles '#' do not pass the light, so the '?' should not be visible for the 2D observer. The trick is to perform two additional visibility checks for the diagonally adjacent tiles, but only for the rays that are not parallel to the X or Y coordinate lines. Parallel rays, which have a 0 in one of their coordinate components, do not require any additional adjacent visibility checks, and the tile, hit by the ray, is always considered visible. For the rays that parallel to the X or Y coordinate lines, the adjacent visibility check always degenerated to the actual ray point visibility check, which is considered invisible if it does not allow light to pass through, and this is the actual bug. To fix the issue, ensure the tile is always considered visible if the ray that hits it is parallel to the X or Y coordinate lines. To better demonstrate the problem, here's a straightforward simulation written in Python: https://gist.github.com/rouming/25c555720f93735442c2053426e73bf5 The code simulates lighting from the DevilitionX implementation, by placing the observer 'x' in the center of the grid. The observer is surrounded by walls and 5 random obstacles, '.' are marked as visible, were hit by the cast rays. The first matrix output shows the bug: no walls and obstacles are visible in the line of sight parallel to the X and Y coordinate lines. In contrast, the second matrix output (with the fix applied) does not exhibit this problem. Also, note the box corners are not visible due to the adjacent visibility checks, which are functioning correctly. Fixes: #6641 Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * lighting: rename variables and add explicit comments for clarity This patch improves clarity and readability without affecting functionality: 1. Renames `VisionCrawlTable` to `VisionRays` and `crawl` to `rayPoint` for better clarity on the purpose of these structures. 2. Renames `factors` to `quadrants` to reflect the actual purpose of the mirror operation along the X or Y coordinate lines. 3. Adds more explicit comments to simplify the understanding of the ray casting algorithm. Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> * test/WarriorLevel1to2: update timedemo Recent visibility fix impacts game state and causes the timedemo to behave completely differently, resulting in a butterfly effect: https://youtu.be/nhpuuHSKGgk. This patch updates the timedemo, which was recorded with the visibility fixes applied, ensuring the tests pass successfully. Here's the latest timedemo video for the future generations: https://youtu.be/udGcWmarYNI Signed-off-by: Roman Penyaev <r.peniaev@gmail.com> --------- Signed-off-by: Roman Penyaev <r.peniaev@gmail.com>
11 months ago
if (!passesLight)
// Tile does not pass the light further, we are
// done with this ray
break;
markTransparentFn(rayPoint);
}
}
}
}
void DoVision(Point position, uint8_t radius, MapExplorationType doAutomap, bool visible)
{
auto markVisibleFn = [doAutomap, visible](Point rayPoint) {
DoVisionFlags(rayPoint, doAutomap, visible);
};
auto markTransparentFn = [](Point rayPoint) {
int8_t trans = dTransVal[rayPoint.x][rayPoint.y];
if (trans != 0)
TransList[trans] = true;
};
auto passesLightFn = [](Point rayPoint) {
return TileAllowsLight(rayPoint);
};
auto inBoundsFn = [](Point rayPoint) {
return InDungeonBounds(rayPoint);
};
DoVision(position, radius, markVisibleFn, markTransparentFn, passesLightFn, inBoundsFn);
}
tl::expected<void, std::string> LoadTrns()
Add dun_render_benchmark Results from a single run (a bit noisy) on my machine: ``` tools/build_and_run_benchmark.py dun_render_benchmark ``` ``` ------------------------------------------------------------------------------------------------------------------------ Benchmark Time CPU Iterations UserCounters... ------------------------------------------------------------------------------------------------------------------------ DunRenderBenchmark/LeftTriangle_Solid_FullyLit 98297 ns 98282 ns 8840 items_per_second=15.1096M/s DunRenderBenchmark/LeftTriangle_Solid_FullyDark 124727 ns 124701 ns 6973 items_per_second=11.9085M/s DunRenderBenchmark/LeftTriangle_Solid_PartiallyLit 514869 ns 514747 ns 1700 items_per_second=2.88491M/s DunRenderBenchmark/LeftTriangle_Transparent_FullyLit 520312 ns 520216 ns 1682 items_per_second=2.85458M/s DunRenderBenchmark/LeftTriangle_Transparent_FullyDark 524440 ns 524331 ns 1664 items_per_second=2.83218M/s DunRenderBenchmark/LeftTriangle_Transparent_PartiallyLit 532300 ns 532162 ns 1647 items_per_second=2.7905M/s DunRenderBenchmark/RightTriangle_Solid_FullyLit 92387 ns 92363 ns 8840 items_per_second=16.7275M/s DunRenderBenchmark/RightTriangle_Solid_FullyDark 85680 ns 85662 ns 9884 items_per_second=18.0361M/s DunRenderBenchmark/RightTriangle_Solid_PartiallyLit 538347 ns 538250 ns 1626 items_per_second=2.87041M/s DunRenderBenchmark/RightTriangle_Transparent_FullyLit 548800 ns 548760 ns 1598 items_per_second=2.81544M/s DunRenderBenchmark/RightTriangle_Transparent_FullyDark 540450 ns 540369 ns 1620 items_per_second=2.85916M/s DunRenderBenchmark/RightTriangle_Transparent_PartiallyLit 555061 ns 555003 ns 1575 items_per_second=2.78377M/s DunRenderBenchmark/TransparentSquare_Solid_FullyLit 700849 ns 700751 ns 1320 items_per_second=3.68176M/s DunRenderBenchmark/TransparentSquare_Solid_FullyDark 664927 ns 664872 ns 1389 items_per_second=3.88045M/s DunRenderBenchmark/TransparentSquare_Solid_PartiallyLit 1131702 ns 1131559 ns 822 items_per_second=2.28004M/s DunRenderBenchmark/TransparentSquare_Transparent_FullyLit 1022384 ns 1022267 ns 916 items_per_second=2.5238M/s DunRenderBenchmark/TransparentSquare_Transparent_FullyDark 1023193 ns 1023057 ns 900 items_per_second=2.52185M/s DunRenderBenchmark/TransparentSquare_Transparent_PartiallyLit 1033573 ns 1033496 ns 895 items_per_second=2.49638M/s DunRenderBenchmark/Square_Solid_FullyLit 53532 ns 53524 ns 10000 items_per_second=30.8272M/s DunRenderBenchmark/Square_Solid_FullyDark 41993 ns 41987 ns 19794 items_per_second=47.1573M/s DunRenderBenchmark/Square_Solid_PartiallyLit 842772 ns 842615 ns 1108 items_per_second=1.56655M/s DunRenderBenchmark/Square_Transparent_FullyLit 834105 ns 834026 ns 1119 items_per_second=1.58269M/s DunRenderBenchmark/Square_Transparent_FullyDark 831912 ns 831823 ns 1122 items_per_second=1.58688M/s DunRenderBenchmark/Square_Transparent_PartiallyLit 924638 ns 924536 ns 1010 items_per_second=1.42774M/s DunRenderBenchmark/LeftTrapezoid_Solid_FullyLit 33728 ns 33725 ns 24962 items_per_second=18.8583M/s DunRenderBenchmark/LeftTrapezoid_Solid_FullyDark 31088 ns 31085 ns 27444 items_per_second=20.4601M/s DunRenderBenchmark/LeftTrapezoid_Solid_PartiallyLit 268792 ns 268768 ns 3254 items_per_second=1.97196M/s DunRenderBenchmark/LeftTrapezoid_Transparent_FullyLit 277990 ns 277965 ns 3140 items_per_second=1.90672M/s DunRenderBenchmark/LeftTrapezoid_Transparent_FullyDark 268952 ns 268912 ns 3250 items_per_second=1.9709M/s DunRenderBenchmark/LeftTrapezoid_Transparent_PartiallyLit 288869 ns 288826 ns 3056 items_per_second=1.83501M/s DunRenderBenchmark/RightTrapezoid_Solid_FullyLit 29581 ns 29576 ns 28347 items_per_second=21.0984M/s DunRenderBenchmark/RightTrapezoid_Solid_FullyDark 25315 ns 25312 ns 32250 items_per_second=24.6523M/s DunRenderBenchmark/RightTrapezoid_Solid_PartiallyLit 259977 ns 259960 ns 3312 items_per_second=2.0003M/s DunRenderBenchmark/RightTrapezoid_Transparent_FullyLit 263079 ns 263056 ns 3322 items_per_second=1.97677M/s DunRenderBenchmark/RightTrapezoid_Transparent_FullyDark 259849 ns 259824 ns 3364 items_per_second=2.00136M/s DunRenderBenchmark/RightTrapezoid_Transparent_PartiallyLit 279623 ns 279594 ns 3127 items_per_second=1.85984M/s ```
2 years ago
{
RETURN_IF_ERROR(LoadFileInMemWithStatus("plrgfx\\infra.trn", InfravisionTable));
RETURN_IF_ERROR(LoadFileInMemWithStatus("plrgfx\\stone.trn", StoneTable));
return LoadFileInMemWithStatus("gendata\\pause.trn", PauseTable);
Add dun_render_benchmark Results from a single run (a bit noisy) on my machine: ``` tools/build_and_run_benchmark.py dun_render_benchmark ``` ``` ------------------------------------------------------------------------------------------------------------------------ Benchmark Time CPU Iterations UserCounters... ------------------------------------------------------------------------------------------------------------------------ DunRenderBenchmark/LeftTriangle_Solid_FullyLit 98297 ns 98282 ns 8840 items_per_second=15.1096M/s DunRenderBenchmark/LeftTriangle_Solid_FullyDark 124727 ns 124701 ns 6973 items_per_second=11.9085M/s DunRenderBenchmark/LeftTriangle_Solid_PartiallyLit 514869 ns 514747 ns 1700 items_per_second=2.88491M/s DunRenderBenchmark/LeftTriangle_Transparent_FullyLit 520312 ns 520216 ns 1682 items_per_second=2.85458M/s DunRenderBenchmark/LeftTriangle_Transparent_FullyDark 524440 ns 524331 ns 1664 items_per_second=2.83218M/s DunRenderBenchmark/LeftTriangle_Transparent_PartiallyLit 532300 ns 532162 ns 1647 items_per_second=2.7905M/s DunRenderBenchmark/RightTriangle_Solid_FullyLit 92387 ns 92363 ns 8840 items_per_second=16.7275M/s DunRenderBenchmark/RightTriangle_Solid_FullyDark 85680 ns 85662 ns 9884 items_per_second=18.0361M/s DunRenderBenchmark/RightTriangle_Solid_PartiallyLit 538347 ns 538250 ns 1626 items_per_second=2.87041M/s DunRenderBenchmark/RightTriangle_Transparent_FullyLit 548800 ns 548760 ns 1598 items_per_second=2.81544M/s DunRenderBenchmark/RightTriangle_Transparent_FullyDark 540450 ns 540369 ns 1620 items_per_second=2.85916M/s DunRenderBenchmark/RightTriangle_Transparent_PartiallyLit 555061 ns 555003 ns 1575 items_per_second=2.78377M/s DunRenderBenchmark/TransparentSquare_Solid_FullyLit 700849 ns 700751 ns 1320 items_per_second=3.68176M/s DunRenderBenchmark/TransparentSquare_Solid_FullyDark 664927 ns 664872 ns 1389 items_per_second=3.88045M/s DunRenderBenchmark/TransparentSquare_Solid_PartiallyLit 1131702 ns 1131559 ns 822 items_per_second=2.28004M/s DunRenderBenchmark/TransparentSquare_Transparent_FullyLit 1022384 ns 1022267 ns 916 items_per_second=2.5238M/s DunRenderBenchmark/TransparentSquare_Transparent_FullyDark 1023193 ns 1023057 ns 900 items_per_second=2.52185M/s DunRenderBenchmark/TransparentSquare_Transparent_PartiallyLit 1033573 ns 1033496 ns 895 items_per_second=2.49638M/s DunRenderBenchmark/Square_Solid_FullyLit 53532 ns 53524 ns 10000 items_per_second=30.8272M/s DunRenderBenchmark/Square_Solid_FullyDark 41993 ns 41987 ns 19794 items_per_second=47.1573M/s DunRenderBenchmark/Square_Solid_PartiallyLit 842772 ns 842615 ns 1108 items_per_second=1.56655M/s DunRenderBenchmark/Square_Transparent_FullyLit 834105 ns 834026 ns 1119 items_per_second=1.58269M/s DunRenderBenchmark/Square_Transparent_FullyDark 831912 ns 831823 ns 1122 items_per_second=1.58688M/s DunRenderBenchmark/Square_Transparent_PartiallyLit 924638 ns 924536 ns 1010 items_per_second=1.42774M/s DunRenderBenchmark/LeftTrapezoid_Solid_FullyLit 33728 ns 33725 ns 24962 items_per_second=18.8583M/s DunRenderBenchmark/LeftTrapezoid_Solid_FullyDark 31088 ns 31085 ns 27444 items_per_second=20.4601M/s DunRenderBenchmark/LeftTrapezoid_Solid_PartiallyLit 268792 ns 268768 ns 3254 items_per_second=1.97196M/s DunRenderBenchmark/LeftTrapezoid_Transparent_FullyLit 277990 ns 277965 ns 3140 items_per_second=1.90672M/s DunRenderBenchmark/LeftTrapezoid_Transparent_FullyDark 268952 ns 268912 ns 3250 items_per_second=1.9709M/s DunRenderBenchmark/LeftTrapezoid_Transparent_PartiallyLit 288869 ns 288826 ns 3056 items_per_second=1.83501M/s DunRenderBenchmark/RightTrapezoid_Solid_FullyLit 29581 ns 29576 ns 28347 items_per_second=21.0984M/s DunRenderBenchmark/RightTrapezoid_Solid_FullyDark 25315 ns 25312 ns 32250 items_per_second=24.6523M/s DunRenderBenchmark/RightTrapezoid_Solid_PartiallyLit 259977 ns 259960 ns 3312 items_per_second=2.0003M/s DunRenderBenchmark/RightTrapezoid_Transparent_FullyLit 263079 ns 263056 ns 3322 items_per_second=1.97677M/s DunRenderBenchmark/RightTrapezoid_Transparent_FullyDark 259849 ns 259824 ns 3364 items_per_second=2.00136M/s DunRenderBenchmark/RightTrapezoid_Transparent_PartiallyLit 279623 ns 279594 ns 3127 items_per_second=1.85984M/s ```
2 years ago
}
void MakeLightTable()
{
// Generate 16 gradually darker translation tables for doing lighting
uint8_t shade = 0;
constexpr uint8_t Black = 0;
constexpr uint8_t White = 255;
for (auto &lightTable : LightTables) {
uint8_t colorIndex = 0;
for (uint8_t steps : { 16, 16, 16, 16, 16, 16, 16, 16, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16 }) {
const uint8_t shading = shade * steps / 16;
const uint8_t shadeStart = colorIndex;
const uint8_t shadeEnd = shadeStart + steps - 1;
for (uint8_t step = 0; step < steps; step++) {
if (colorIndex == Black) {
lightTable[colorIndex++] = Black;
continue;
}
int color = shadeStart + step + shading;
if (color > shadeEnd || color == White)
color = Black;
lightTable[colorIndex++] = color;
}
}
shade++;
}
LightTables[15] = {}; // Make last shade pitch black
FullyLitLightTable = LightTables[0].data();
FullyDarkLightTable = LightTables[LightsMax].data();
7 years ago
if (leveltype == DTYPE_HELL) {
// Blood wall lighting
const auto shades = static_cast<int>(LightTables.size() - 1);
for (int i = 0; i < shades; i++) {
auto &lightTable = LightTables[i];
constexpr int Range = 16;
for (int j = 0; j < Range; j++) {
uint8_t color = ((Range - 1) << 4) / shades * (shades - i) / Range * (j + 1);
color = 1 + (color >> 4);
int idx = j + 1;
lightTable[idx] = color;
idx = 31 - j;
lightTable[idx] = color;
}
}
FullyLitLightTable = nullptr; // A color map is used for the ceiling animation, so even fully lit tiles have a color map
} else if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) {
// Make the lava fully bright
for (auto &lightTable : LightTables)
std::iota(lightTable.begin(), lightTable.begin() + 16, uint8_t { 0 });
LightTables[15][0] = 0;
std::fill_n(LightTables[15].begin() + 1, 15, 1);
FullyDarkLightTable = nullptr; // Tiles in Hellfire levels are never completely black
}
// Verify that fully lit and fully dark light table optimizations are correctly enabled/disabled (nullptr = disabled)
assert((FullyLitLightTable != nullptr) == (LightTables[0][0] == 0 && std::adjacent_find(LightTables[0].begin(), LightTables[0].end() - 1, [](auto x, auto y) { return (x + 1) != y; }) == LightTables[0].end() - 1));
assert((FullyDarkLightTable != nullptr) == (std::all_of(LightTables[LightsMax].begin(), LightTables[LightsMax].end(), [](auto x) { return x == 0; })));
// Generate light falloffs ranges
const float maxDarkness = 15;
const float maxBrightness = 0;
for (unsigned radius = 0; radius < NumLightRadiuses; radius++) {
const unsigned maxDistance = (radius + 1) * 8;
for (unsigned distance = 0; distance < 128; distance++) {
if (distance > maxDistance) {
LightFalloffs[radius][distance] = 15;
} else {
const float factor = static_cast<float>(distance) / static_cast<float>(maxDistance);
float scaled;
if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) {
// quardratic falloff with over exposure
const float brightness = static_cast<float>(radius) * 1.25F;
scaled = factor * factor * brightness + (maxDarkness - brightness);
scaled = std::max(maxBrightness, scaled);
} else {
// Leaner falloff
scaled = factor * maxDarkness;
}
scaled += 0.5F; // Round up
LightFalloffs[radius][distance] = static_cast<uint8_t>(scaled);
}
}
}
// Generate the light cone interpolations
for (int offsetY = 0; offsetY < 8; offsetY++) {
for (int offsetX = 0; offsetX < 8; offsetX++) {
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
int a = (8 * x - offsetX);
int b = (8 * y - offsetY);
LightConeInterpolations[offsetX][offsetY][x][y] = static_cast<uint8_t>(sqrt(a * a + b * b));
}
}
}
}
}
#ifdef _DEBUG
void ToggleLighting()
{
DisableLighting = !DisableLighting;
if (DisableLighting) {
memset(dLight, 0, sizeof(dLight));
return;
}
memcpy(dLight, dPreLight, sizeof(dLight));
for (const Player &player : Players) {
if (player.plractive && player.isOnActiveLevel()) {
DoLighting(player.position.tile, player._pLightRad, {});
}
}
}
#endif
void InitLighting()
{
ActiveLightCount = 0;
UpdateLighting = false;
UpdateVision = false;
#ifdef _DEBUG
DisableLighting = false;
#endif
std::iota(ActiveLights.begin(), ActiveLights.end(), uint8_t { 0 });
VisionActive = {};
TransList = {};
}
int AddLight(Point position, uint8_t radius)
{
#ifdef _DEBUG
if (DisableLighting)
return NO_LIGHT;
#endif
if (ActiveLightCount >= MAXLIGHTS)
return NO_LIGHT;
int lid = ActiveLights[ActiveLightCount++];
Light &light = Lights[lid];
light.position.tile = position;
light.radius = radius;
light.position.offset = { 0, 0 };
light.isInvalid = false;
light.hasChanged = false;
UpdateLighting = true;
return lid;
}
void AddUnLight(int i)
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (i == NO_LIGHT)
return;
Lights[i].isInvalid = true;
UpdateLighting = true;
}
void ChangeLightRadius(int i, uint8_t radius)
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (i == NO_LIGHT)
return;
Light &light = Lights[i];
light.hasChanged = true;
light.position.old = light.position.tile;
light.oldRadius = light.radius;
light.radius = radius;
UpdateLighting = true;
}
void ChangeLightXY(int i, Point position)
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (i == NO_LIGHT)
return;
Light &light = Lights[i];
light.hasChanged = true;
light.position.old = light.position.tile;
light.oldRadius = light.radius;
light.position.tile = position;
UpdateLighting = true;
}
void ChangeLightOffset(int i, DisplacementOf<int8_t> offset)
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (i == NO_LIGHT)
return;
Light &light = Lights[i];
if (light.position.offset == offset)
return;
light.hasChanged = true;
light.position.old = light.position.tile;
light.oldRadius = light.radius;
light.position.offset = offset;
UpdateLighting = true;
}
void ChangeLight(int i, Point position, uint8_t radius)
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (i == NO_LIGHT)
return;
Light &light = Lights[i];
light.hasChanged = true;
light.position.old = light.position.tile;
light.oldRadius = light.radius;
light.position.tile = position;
light.radius = radius;
UpdateLighting = true;
}
void ProcessLightList()
{
#ifdef _DEBUG
if (DisableLighting)
return;
#endif
if (!UpdateLighting)
return;
for (int i = 0; i < ActiveLightCount; i++) {
Light &light = Lights[ActiveLights[i]];
if (light.isInvalid) {
DoUnLight(light.position.tile, light.radius);
}
if (light.hasChanged) {
DoUnLight(light.position.old, light.oldRadius);
light.hasChanged = false;
}
}
for (int i = 0; i < ActiveLightCount; i++) {
const Light &light = Lights[ActiveLights[i]];
if (light.isInvalid) {
ActiveLightCount--;
std::swap(ActiveLights[ActiveLightCount], ActiveLights[i]);
i--;
continue;
}
if (TileHasAny(light.position.tile, TileProperties::Solid))
continue; // Monster hidden in a wall, don't spoil the surprise
DoLighting(light.position.tile, light.radius, light.position.offset);
}
UpdateLighting = false;
}
void SavePreLighting()
{
memcpy(dPreLight, dLight, sizeof(dPreLight));
}
void ActivateVision(Point position, int r, size_t id)
{
auto &vision = VisionList[id];
vision.position.tile = position;
vision.radius = r;
vision.isInvalid = false;
vision.hasChanged = false;
VisionActive[id] = true;
UpdateVision = true;
}
void ChangeVisionRadius(size_t id, int r)
{
auto &vision = VisionList[id];
vision.hasChanged = true;
vision.position.old = vision.position.tile;
vision.oldRadius = vision.radius;
vision.radius = r;
UpdateVision = true;
}
void ChangeVisionXY(size_t id, Point position)
{
auto &vision = VisionList[id];
vision.hasChanged = true;
vision.position.old = vision.position.tile;
vision.oldRadius = vision.radius;
vision.position.tile = position;
UpdateVision = true;
}
void ProcessVisionList()
{
if (!UpdateVision)
return;
TransList = {};
for (const Player &player : Players) {
const size_t id = player.getId();
if (!VisionActive[id])
continue;
Light &vision = VisionList[id];
if (!player.plractive || !player.isOnActiveLevel() || (player._pLvlChanging && &player != MyPlayer)) {
DoUnVision(vision.position.tile, vision.radius);
VisionActive[id] = false;
continue;
}
if (vision.hasChanged) {
DoUnVision(vision.position.old, vision.oldRadius);
vision.hasChanged = false;
}
}
for (const Player &player : Players) {
const size_t id = player.getId();
if (!VisionActive[id])
continue;
Light &vision = VisionList[id];
MapExplorationType doautomap = MAP_EXP_SELF;
if (&player != MyPlayer)
doautomap = player.friendlyMode ? MAP_EXP_OTHERS : MAP_EXP_NONE;
DoVision(
vision.position.tile,
vision.radius,
doautomap,
&player == MyPlayer);
}
UpdateVision = false;
}
void lighting_color_cycling()
{
for (auto &lightTable : LightTables) {
// shift elements between indexes 1-31 to left
std::rotate(lightTable.begin() + 1, lightTable.begin() + 2, lightTable.begin() + 32);
}
}
} // namespace devilution