diff --git a/Source/lighting.cpp b/Source/lighting.cpp index c8b6fe9c9..e89f459fb 100644 --- a/Source/lighting.cpp +++ b/Source/lighting.cpp @@ -41,10 +41,15 @@ bool UpdateLighting; namespace { /* - * X- Y-coordinate offsets of lighting visions. - * The last entry-pair is only for alignment. + * 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 */ -const DisplacementOf VisionCrawlTable[23][15] = { +static const DisplacementOf 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 } }, @@ -80,9 +85,6 @@ 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]; -/** RadiusAdj maps from VisionCrawlTable index to lighting vision radius adjustment. */ -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 }; - void RotateRadius(DisplacementOf &offset, DisplacementOf &dist, DisplacementOf &light, DisplacementOf &block) { dist = { static_cast(7 - dist.deltaY), dist.deltaX }; @@ -232,31 +234,67 @@ void DoVision(Point position, uint8_t radius, MapExplorationType doAutomap, bool { DoVisionFlags(position, doAutomap, visible); - static const Displacement factors[] = { { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 } }; - for (auto factor : factors) { - for (int j = 0; j < 23; j++) { - int lineLen = radius - RadiusAdj[j]; - for (int k = 0; k < lineLen; k++) { - Point crawl = position + VisionCrawlTable[j][k] * factor; - if (!InDungeonBounds(crawl)) + // 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 (!InDungeonBounds(rayPoint)) break; - bool blockerFlag = TileHasAny(crawl, TileProperties::BlockLight); - bool tileOK = !blockerFlag; - if (VisionCrawlTable[j][k].deltaX > 0 && VisionCrawlTable[j][k].deltaY > 0) { - tileOK = tileOK || TileAllowsLight(crawl + Displacement { -factor.deltaX, 0 }); - tileOK = tileOK || TileAllowsLight(crawl + Displacement { 0, -factor.deltaY }); + bool visible = true; + + // + // 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 }; + + visible = (TileAllowsLight(rayPoint + adjacent1) || TileAllowsLight(rayPoint + adjacent2)); } + if (visible) + DoVisionFlags(rayPoint, doAutomap, visible); - if (!tileOK) - break; - - DoVisionFlags(crawl, doAutomap, visible); - - if (blockerFlag) + bool passesLight = TileAllowsLight(rayPoint); + if (!passesLight) + // Tile does not pass the light further, we are + // done with this ray break; - int8_t trans = dTransVal[crawl.x][crawl.y]; + int8_t trans = dTransVal[rayPoint.x][rayPoint.y]; if (trans != 0) TransList[trans] = true; } diff --git a/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo b/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo index 33c8812f2..cd0148900 100644 Binary files a/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo and b/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo differ diff --git a/test/fixtures/timedemo/WarriorLevel1to2/demo_0_reference_spawn_0.sv b/test/fixtures/timedemo/WarriorLevel1to2/demo_0_reference_spawn_0.sv index 8aaa31633..701139941 100644 Binary files a/test/fixtures/timedemo/WarriorLevel1to2/demo_0_reference_spawn_0.sv and b/test/fixtures/timedemo/WarriorLevel1to2/demo_0_reference_spawn_0.sv differ diff --git a/test/fixtures/timedemo/WarriorLevel1to2/spawn_0.sv b/test/fixtures/timedemo/WarriorLevel1to2/spawn_0.sv index db7d4c4e1..4521d2c0c 100644 Binary files a/test/fixtures/timedemo/WarriorLevel1to2/spawn_0.sv and b/test/fixtures/timedemo/WarriorLevel1to2/spawn_0.sv differ