/** * @file lighting.cpp * * Implementation of light and vision. */ #include "lighting.h" #include #include #include "automap.h" #include "diablo.h" #include "engine/load_file.hpp" #include "engine/points_in_rectangle_range.hpp" #include "player.h" namespace devilution { std::array VisionActive; Light VisionList[MAXVISION]; Light Lights[MAXLIGHTS]; std::array ActiveLights; int ActiveLightCount; std::array, NumLightingLevels> LightTables; std::array InfravisionTable; std::array StoneTable; std::array PauseTable; #ifdef _DEBUG bool DisableLighting; #endif bool UpdateLighting; namespace { /* * X- Y-coordinate offsets of lighting visions. * The last entry-pair is only for alignment. */ const DisplacementOf VisionCrawlTable[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]; /** 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 }; light = { static_cast(7 - light.deltaY), light.deltaX }; offset = { static_cast(dist.deltaX - light.deltaX), static_cast(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; } } void SetLight(Point position, uint8_t v) { if (LoadingMapObjects) dPreLight[position.x][position.y] = v; else dLight[position.x][position.y] = v; } uint8_t GetLight(Point position) { if (LoadingMapObjects) return dPreLight[position.x][position.y]; return dLight[position.x][position.y]; } 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]; } } bool CrawlFlipsX(Displacement mirrored, tl::function_ref function) { for (const Displacement displacement : { mirrored.flipX(), mirrored }) { if (!function(displacement)) return false; } return true; } bool CrawlFlipsY(Displacement mirrored, tl::function_ref function) { for (const Displacement displacement : { mirrored, mirrored.flipY() }) { if (!function(displacement)) return false; } return true; } bool CrawlFlipsXY(Displacement mirrored, tl::function_ref function) { for (const Displacement displacement : { mirrored.flipX(), mirrored, mirrored.flipXY(), mirrored.flipY() }) { if (!function(displacement)) return false; } return true; } bool TileAllowsLight(Point position) { if (!InDungeonBounds(position)) return false; return !TileHasAny(dPiece[position.x][position.y], 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 bool DoCrawl(unsigned radius, tl::function_ref function) { if (radius == 0) return function(Displacement { 0, 0 }); if (!CrawlFlipsY({ 0, static_cast(radius) }, function)) return false; for (unsigned i = 1; i < radius; i++) { if (!CrawlFlipsXY({ static_cast(i), static_cast(radius) }, function)) return false; } if (radius > 1) { if (!CrawlFlipsXY({ static_cast(radius) - 1, static_cast(radius) - 1 }, function)) return false; } if (!CrawlFlipsX({ static_cast(radius), 0 }, function)) return false; for (unsigned i = 1; i < radius; i++) { if (!CrawlFlipsXY({ static_cast(radius), static_cast(i) }, function)) return false; } return true; } bool DoCrawl(unsigned minRadius, unsigned maxRadius, tl::function_ref function) { for (unsigned i = minRadius; i <= maxRadius; i++) { if (!DoCrawl(i, function)) return false; } return true; } void DoLighting(Point position, uint8_t radius, DisplacementOf offset) { assert(radius >= 0 && radius <= NumLightRadiuses); assert(InDungeonBounds(position)); DisplacementOf light = {}; DisplacementOf block = {}; if (offset.deltaX < 0) { offset.deltaX += 8; position -= { 1, 0 }; } if (offset.deltaY < 0) { offset.deltaY += 8; position -= { 0, 1 }; } DisplacementOf 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; } if (IsNoneOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { SetLight(position, 0); } else if (GetLight(position) > LightFalloffs[radius][0]) { SetLight(position, LightFalloffs[radius][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, MapExplorationType doAutomap, bool visible) { 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)) break; bool blockerFlag = TileHasAny(dPiece[crawl.x][crawl.y], 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 }); } if (!tileOK) break; DoVisionFlags(crawl, doAutomap, visible); if (blockerFlag) break; int8_t trans = dTransVal[crawl.x][crawl.y]; if (trans != 0) TransList[trans] = true; } } } } 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 if (leveltype == DTYPE_HELL) { // Blood wall lighting const int shades = 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); lightTable[j + 1] = color; lightTable[31 - j] = color; } } } else if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { // Make the lava fully bright for (auto &lightTable : LightTables) std::iota(lightTable.begin(), lightTable.begin() + 16, 0); LightTables[15][0] = 0; std::fill_n(LightTables[15].begin() + 1, 15, 1); } LoadFileInMem("plrgfx\\infra.trn", InfravisionTable); LoadFileInMem("plrgfx\\stone.trn", StoneTable); LoadFileInMem("gendata\\pause.trn", PauseTable); // Generate light falloffs ranges if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { for (size_t j = 0; j < NumLightRadiuses; j++) { double fa = (sqrt((double)(16 - j))) / 128; fa *= fa; for (int i = 0; i < 128; i++) { uint8_t color = 15 - static_cast(fa * (double)((128 - i) * (128 - i))); if (color > 15) color = 0; color -= static_cast((NumLightRadiuses - j - 1) / 2); if (color > 15) color = 0; LightFalloffs[NumLightRadiuses - j - 1][i] = color; } } } else { for (size_t j = 0; j < NumLightRadiuses; j++) { for (size_t i = 0; i < 128; i++) { if (i > (j + 1) * 8) { LightFalloffs[j][i] = 15; } else { double fs = (double)15 * i / ((double)8 * (j + 1)); LightFalloffs[j][i] = static_cast(fs + 0.5); } } } } // 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 - offsetY); int b = (8 * y - offsetX); LightConeInterpolations[offsetX][offsetY][x][y] = static_cast(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(), 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, Displacement offset) { #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.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; } DoLighting(light.position.tile, light.radius, light.position.offset); } UpdateLighting = false; } void SavePreLighting() { memcpy(dPreLight, dLight, sizeof(dPreLight)); } void ActivateVision(Point position, int r, int 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(int 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(int 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) { int id = player.getId(); if (!VisionActive[id]) continue; Light &vision = VisionList[id]; if (!player.plractive || !player.isOnActiveLevel()) { DoUnVision(vision.position.tile, vision.radius); VisionActive[id] = false; continue; } if (vision.hasChanged) { DoUnVision(vision.position.old, vision.oldRadius); vision.hasChanged = false; } 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