Skip to content

Commit a27926a

Browse files
roumingTrihedraf
authored andcommitted
lighting: fix long-standing issue with invisible objects (diasurgical#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 diasurgical#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: diasurgical#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>
1 parent a8392f2 commit a27926a

4 files changed

Lines changed: 63 additions & 25 deletions

File tree

Source/lighting.cpp

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@ bool UpdateLighting;
4141
namespace {
4242

4343
/*
44-
* X- Y-coordinate offsets of lighting visions.
45-
* The last entry-pair is only for alignment.
44+
* XY points of vision rays are cast to trace the visibility of the
45+
* surrounding environment. The table represents N rays of M points in
46+
* one quadrant (0°-90°) of a circle, so rays for other quadrants will
47+
* be created by mirroring. Zero points at the end will be trimmed and
48+
* ignored. A similar table can be recreated using Bresenham's line
49+
* drawing algorithm, which is suitable for integer arithmetic:
50+
* https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
4651
*/
47-
const DisplacementOf<int8_t> VisionCrawlTable[23][15] = {
52+
static const DisplacementOf<int8_t> VisionRays[23][15] = {
4853
// clang-format off
4954
{ { 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 } },
5055
{ { 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 } },
@@ -80,9 +85,6 @@ bool UpdateVision;
8085
/** interpolations of a 32x32 (16x16 mirrored) light circle moving between tiles in steps of 1/8 of a tile */
8186
uint8_t LightConeInterpolations[8][8][16][16];
8287

83-
/** RadiusAdj maps from VisionCrawlTable index to lighting vision radius adjustment. */
84-
const uint8_t RadiusAdj[23] = { 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 4, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0 };
85-
8688
void RotateRadius(DisplacementOf<int8_t> &offset, DisplacementOf<int8_t> &dist, DisplacementOf<int8_t> &light, DisplacementOf<int8_t> &block)
8789
{
8890
dist = { static_cast<int8_t>(7 - dist.deltaY), dist.deltaX };
@@ -232,31 +234,67 @@ void DoVision(Point position, uint8_t radius, MapExplorationType doAutomap, bool
232234
{
233235
DoVisionFlags(position, doAutomap, visible);
234236

235-
static const Displacement factors[] = { { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 } };
236-
for (auto factor : factors) {
237-
for (int j = 0; j < 23; j++) {
238-
int lineLen = radius - RadiusAdj[j];
239-
for (int k = 0; k < lineLen; k++) {
240-
Point crawl = position + VisionCrawlTable[j][k] * factor;
241-
if (!InDungeonBounds(crawl))
237+
// Adjustment to a ray length to ensure all rays lie on an
238+
// accurate circle
239+
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 };
240+
static_assert(std::size(rayLenAdj) == std::size(VisionRays));
241+
242+
// Four quadrants on a circle
243+
static const Displacement quadrants[] = { { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 } };
244+
245+
// Loop over quadrants and mirror rays for each one
246+
for (const auto &quadrant : quadrants) {
247+
// Cast a ray for a quadrant
248+
for (unsigned int j = 0; j < std::size(VisionRays); j++) {
249+
int rayLen = radius - rayLenAdj[j];
250+
for (int k = 0; k < rayLen; k++) {
251+
const auto &relRayPoint = VisionRays[j][k];
252+
// Calculate the next point on a ray in the quadrant
253+
Point rayPoint = position + relRayPoint * quadrant;
254+
if (!InDungeonBounds(rayPoint))
242255
break;
243-
bool blockerFlag = TileHasAny(crawl, TileProperties::BlockLight);
244-
bool tileOK = !blockerFlag;
245256

246-
if (VisionCrawlTable[j][k].deltaX > 0 && VisionCrawlTable[j][k].deltaY > 0) {
247-
tileOK = tileOK || TileAllowsLight(crawl + Displacement { -factor.deltaX, 0 });
248-
tileOK = tileOK || TileAllowsLight(crawl + Displacement { 0, -factor.deltaY });
257+
bool visible = true;
258+
259+
//
260+
// We've cast an approximated ray on an integer 2D
261+
// grid, so we need to check if a ray can pass through
262+
// the diagonally adjacent tiles. For example, consider
263+
// this case:
264+
//
265+
// #?
266+
// ↗ #
267+
// x
268+
//
269+
// The ray is cast from the observer 'x', and reaches
270+
// the '?', but diagonally adjacent tiles '#' do not
271+
// pass the light, so the '?' should not be visible
272+
// for the 2D observer.
273+
//
274+
// The trick is to perform two additional visibility
275+
// checks for the diagonally adjacent tiles, but only
276+
// for the rays that are not parallel to the X or Y
277+
// coordinate lines. Parallel rays, which have a 0 in
278+
// one of their coordinate components, do not require
279+
// any additional adjacent visibility checks, and the
280+
// tile, hit by the ray, is always considered visible.
281+
//
282+
if (relRayPoint.deltaX > 0 && relRayPoint.deltaY > 0) {
283+
Displacement adjacent1 = { -quadrant.deltaX, 0 };
284+
Displacement adjacent2 = { 0, -quadrant.deltaY };
285+
286+
visible = (TileAllowsLight(rayPoint + adjacent1) || TileAllowsLight(rayPoint + adjacent2));
249287
}
288+
if (visible)
289+
DoVisionFlags(rayPoint, doAutomap, visible);
250290

251-
if (!tileOK)
252-
break;
253-
254-
DoVisionFlags(crawl, doAutomap, visible);
255-
256-
if (blockerFlag)
291+
bool passesLight = TileAllowsLight(rayPoint);
292+
if (!passesLight)
293+
// Tile does not pass the light further, we are
294+
// done with this ray
257295
break;
258296

259-
int8_t trans = dTransVal[crawl.x][crawl.y];
297+
int8_t trans = dTransVal[rayPoint.x][rayPoint.y];
260298
if (trans != 0)
261299
TransList[trans] = true;
262300
}
-205 KB
Binary file not shown.
Binary file not shown.
250 KB
Binary file not shown.

0 commit comments

Comments
 (0)