#pragma once #include "StarList.hpp" #include "StarVector.hpp" namespace Star { // Operations for simple scalar lighting. struct ScalarLightTraits { typedef float Value; static float spread(float source, float dest, float drop); static float subtract(float value, float drop); static float multiply(float v1, float v2); static float maxIntensity(float value); static float minIntensity(float value); static float max(float v1, float v2); }; // Operations for 3 component (colored) lighting. Spread and subtract are // applied proportionally, so that color ratios stay the same, to prevent hues // changing as light spreads. struct ColoredLightTraits { typedef Vec3F Value; static Vec3F spread(Vec3F const& source, Vec3F const& dest, float drop); static Vec3F subtract(Vec3F value, float drop); static Vec3F multiply(Vec3F value, float drop); static float maxIntensity(Vec3F const& value); static float minIntensity(Vec3F const& value); static Vec3F max(Vec3F const& v1, Vec3F const& v2); }; template class CellularLightArray { public: typedef typename LightTraits::Value LightValue; struct Cell { LightValue light; bool obstacle; }; struct SpreadLight { Vec2F position; LightValue value; }; struct PointLight { Vec2F position; LightValue value; float beam; float beamAngle; float beamAmbience; bool asSpread; }; void setParameters(unsigned spreadPasses, float spreadMaxAir, float spreadMaxObstacle, float pointMaxAir, float pointMaxObstacle, float pointObstacleBoost); // The border around the target lighting array where initial lighting / light // source data is required. Based on parameters. size_t borderCells() const; // Begin a new calculation, setting internal storage to new width and height // (if these are the same as last time this is cheap). Always clears all // existing light and collision data. void begin(size_t newWidth, size_t newHeight); // Position is in index space, spread lights will have no effect if they are // outside of the array. Integer points are assumed to be on the corners of // the grid (not the center) void addSpreadLight(SpreadLight const& spreadLight); void addPointLight(PointLight const& pointLight); // Directly set the lighting values for this position. void setLight(size_t x, size_t y, LightValue const& light); // Get current light value. Call after calling calculate() to pull final // data out. LightValue getLight(size_t x, size_t y) const; // Set obstacle values for this position void setObstacle(size_t x, size_t y, bool obstacle); bool getObstacle(size_t x, size_t y) const; Cell const& cell(size_t x, size_t y) const; Cell& cell(size_t x, size_t y); Cell const& cellAtIndex(size_t index) const; Cell& cellAtIndex(size_t index); // Calculate lighting in the given sub-rect, in order to properly do spread // lighting, and initial lighting must be given for the ambient border this // given rect, and the array size must be at least that large. xMax / yMax // are not inclusive, the range is [xMin, xMax) and [yMin, yMax). void calculate(size_t xMin, size_t yMin, size_t xMax, size_t yMax); private: // Set 4 points based on interpolated light position and free space // attenuation. void setSpreadLightingPoints(); // Spreads light out in an octagonal based cellular automata void calculateLightSpread(size_t xmin, size_t ymin, size_t xmax, size_t ymax); // Loops through each light and adds light strength based on distance and // obstacle attenuation. Calculates within the given sub-rect void calculatePointLighting(size_t xmin, size_t ymin, size_t xmax, size_t ymax); // Run Xiaolin Wu's anti-aliased line drawing algorithm from start to end, // summing each block that would be drawn to to produce an attenuation. Not // circularized. float lineAttenuation(Vec2F const& start, Vec2F const& end, float perObstacleAttenuation, float maxAttenuation); size_t m_width; size_t m_height; unique_ptr m_cells; List m_spreadLights; List m_pointLights; unsigned m_spreadPasses; float m_spreadMaxAir; float m_spreadMaxObstacle; float m_pointMaxAir; float m_pointMaxObstacle; float m_pointObstacleBoost; }; typedef CellularLightArray ColoredCellularLightArray; typedef CellularLightArray ScalarCellularLightArray; inline float ScalarLightTraits::spread(float source, float dest, float drop) { return std::max(source - drop, dest); } inline float ScalarLightTraits::subtract(float c, float drop) { return std::max(c - drop, 0.0f); } inline float ScalarLightTraits::multiply(float v1, float v2) { return v1 * v2; } inline float ScalarLightTraits::maxIntensity(float value) { return value; } inline float ScalarLightTraits::minIntensity(float value) { return value; } inline float ScalarLightTraits::max(float v1, float v2) { return std::max(v1, v2); } inline Vec3F ColoredLightTraits::spread(Vec3F const& source, Vec3F const& dest, float drop) { float maxChannel = std::max(source[0], std::max(source[1], source[2])); if (maxChannel <= 0.0f) return dest; drop /= maxChannel; return Vec3F( std::max(source[0] - source[0] * drop, dest[0]), std::max(source[1] - source[1] * drop, dest[1]), std::max(source[2] - source[2] * drop, dest[2]) ); } inline Vec3F ColoredLightTraits::subtract(Vec3F c, float drop) { float max = std::max(std::max(c[0], c[1]), c[2]); if (max <= 0.0f) return c; for (size_t i = 0; i < 3; ++i) { float pdrop = (drop * c[i]) / max; if (c[i] > pdrop) c[i] -= pdrop; else c[i] = 0; } return c; } inline Vec3F ColoredLightTraits::multiply(Vec3F c, float drop) { return c * drop; } inline float ColoredLightTraits::maxIntensity(Vec3F const& value) { return value.max(); } inline float ColoredLightTraits::minIntensity(Vec3F const& value) { return value.min(); } inline Vec3F ColoredLightTraits::max(Vec3F const& v1, Vec3F const& v2) { return vmax(v1, v2); } template void CellularLightArray::setParameters(unsigned spreadPasses, float spreadMaxAir, float spreadMaxObstacle, float pointMaxAir, float pointMaxObstacle, float pointObstacleBoost) { m_spreadPasses = spreadPasses; m_spreadMaxAir = spreadMaxAir; m_spreadMaxObstacle = spreadMaxObstacle; m_pointMaxAir = pointMaxAir; m_pointMaxObstacle = pointMaxObstacle; m_pointObstacleBoost = pointObstacleBoost; } template size_t CellularLightArray::borderCells() const { return (size_t)ceil(max(0.0f, max(m_spreadMaxAir, m_pointMaxAir))); } template void CellularLightArray::begin(size_t newWidth, size_t newHeight) { m_spreadLights.clear(); m_pointLights.clear(); starAssert(newWidth > 0 && newHeight > 0); if (!m_cells || newWidth != m_width || newHeight != m_height) { m_width = newWidth; m_height = newHeight; m_cells.reset(new Cell[m_width * m_height]()); } else { std::fill(m_cells.get(), m_cells.get() + m_width * m_height, Cell{LightValue{}, false}); } } template void CellularLightArray::addSpreadLight(SpreadLight const& spreadLight) { m_spreadLights.append(spreadLight); } template void CellularLightArray::addPointLight(PointLight const& pointLight) { m_pointLights.append(pointLight); } template void CellularLightArray::setLight(size_t x, size_t y, LightValue const& lightValue) { cell(x, y).light = lightValue; } template void CellularLightArray::setObstacle(size_t x, size_t y, bool obstacle) { cell(x, y).obstacle = obstacle; } template auto CellularLightArray::getLight(size_t x, size_t y) const -> LightValue { return cell(x, y).light; } template bool CellularLightArray::getObstacle(size_t x, size_t y) const { return cell(x, y).obstacle; } template auto CellularLightArray::cell(size_t x, size_t y) const -> Cell const & { starAssert(x < m_width && y < m_height); return m_cells[x * m_height + y]; } template auto CellularLightArray::cell(size_t x, size_t y) -> Cell & { starAssert(x < m_width && y < m_height); return m_cells[x * m_height + y]; } template auto CellularLightArray::cellAtIndex(size_t index) const -> Cell const & { starAssert(index < m_width * m_height); return m_cells[index]; } template auto CellularLightArray::cellAtIndex(size_t index) -> Cell & { starAssert(index < m_width * m_height); return m_cells[index]; } template void CellularLightArray::calculate(size_t xMin, size_t yMin, size_t xMax, size_t yMax) { setSpreadLightingPoints(); calculateLightSpread(xMin, yMin, xMax, yMax); calculatePointLighting(xMin, yMin, xMax, yMax); } template void CellularLightArray::setSpreadLightingPoints() { for (SpreadLight const& light : m_spreadLights) { // - 0.5f to correct for lights being on the grid corners and not center int minX = floor(light.position[0] - 0.5f); int minY = floor(light.position[1] - 0.5f); int maxX = minX + 1; int maxY = minY + 1; float xdist = light.position[0] - minX - 0.5f; float ydist = light.position[1] - minY - 0.5f; // Pick falloff here based on closest block obstacle value (probably not // best) Vec2I pos(light.position.floor()); float oneBlockAtt; if (pos[0] >= 0 && pos[0] < (int)m_width && pos[1] >= 0 && pos[1] < (int)m_height && getObstacle(pos[0], pos[1])) oneBlockAtt = 1.0f / m_spreadMaxObstacle; else oneBlockAtt = 1.0f / m_spreadMaxAir; // "pre fall-off" a 2x2 area of blocks to smooth out floating point // positions using the cellular algorithm if (minX >= 0 && minX < (int)m_width && minY >= 0 && minY < (int)m_height) setLight(minX, minY, LightTraits::max(getLight(minX, minY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (1.0f - xdist) - (1.0f - ydist))))); if (minX >= 0 && minX < (int)m_width && maxY >= 0 && maxY < (int)m_height) setLight(minX, maxY, LightTraits::max(getLight(minX, maxY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (1.0f - xdist) - (ydist))))); if (maxX >= 0 && maxX < (int)m_width && minY >= 0 && minY < (int)m_height) setLight(maxX, minY, LightTraits::max(getLight(maxX, minY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (xdist) - (1.0f - ydist))))); if (maxX >= 0 && maxX < (int)m_width && maxY >= 0 && maxY < (int)m_height) setLight(maxX, maxY, LightTraits::max(getLight(maxX, maxY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (xdist) - (ydist))))); } } template void CellularLightArray::calculateLightSpread(size_t xMin, size_t yMin, size_t xMax, size_t yMax) { starAssert(m_width > 0 && m_height > 0); float dropoffAir = 1.0f / m_spreadMaxAir; float dropoffObstacle = 1.0f / m_spreadMaxObstacle; float dropoffAirDiag = 1.0f / m_spreadMaxAir * Constants::sqrt2; float dropoffObstacleDiag = 1.0f / m_spreadMaxObstacle * Constants::sqrt2; // enlarge x/y min/max taking into ambient spread of light xMin = xMin - min(xMin, (size_t)ceil(m_spreadMaxAir)); yMin = yMin - min(yMin, (size_t)ceil(m_spreadMaxAir)); xMax = min(m_width, xMax + (size_t)ceil(m_spreadMaxAir)); yMax = min(m_height, yMax + (size_t)ceil(m_spreadMaxAir)); for (unsigned p = 0; p < m_spreadPasses; ++p) { // Spread right and up and diag up right / diag down right for (size_t x = xMin + 1; x < xMax - 1; ++x) { size_t xCellOffset = x * m_height; size_t xRightCellOffset = (x + 1) * m_height; for (size_t y = yMin + 1; y < yMax - 1; ++y) { auto cell = cellAtIndex(xCellOffset + y); auto& cellRight = cellAtIndex(xRightCellOffset + y); auto& cellUp = cellAtIndex(xCellOffset + y + 1); auto& cellRightUp = cellAtIndex(xRightCellOffset + y + 1); auto& cellRightDown = cellAtIndex(xRightCellOffset + y - 1); float straightDropoff = cell.obstacle ? dropoffObstacle : dropoffAir; float diagDropoff = cell.obstacle ? dropoffObstacleDiag : dropoffAirDiag; cellRight.light = LightTraits::spread(cell.light, cellRight.light, straightDropoff); cellUp.light = LightTraits::spread(cell.light, cellUp.light, straightDropoff); cellRightUp.light = LightTraits::spread(cell.light, cellRightUp.light, diagDropoff); cellRightDown.light = LightTraits::spread(cell.light, cellRightDown.light, diagDropoff); } } // Spread left and down and diag up left / diag down left for (size_t x = xMax - 2; x > xMin; --x) { size_t xCellOffset = x * m_height; size_t xLeftCellOffset = (x - 1) * m_height; for (size_t y = yMax - 2; y > yMin; --y) { auto cell = cellAtIndex(xCellOffset + y); auto& cellLeft = cellAtIndex(xLeftCellOffset + y); auto& cellDown = cellAtIndex(xCellOffset + y - 1); auto& cellLeftUp = cellAtIndex(xLeftCellOffset + y + 1); auto& cellLeftDown = cellAtIndex(xLeftCellOffset + y - 1); float straightDropoff = cell.obstacle ? dropoffObstacle : dropoffAir; float diagDropoff = cell.obstacle ? dropoffObstacleDiag : dropoffAirDiag; cellLeft.light = LightTraits::spread(cell.light, cellLeft.light, straightDropoff); cellDown.light = LightTraits::spread(cell.light, cellDown.light, straightDropoff); cellLeftUp.light = LightTraits::spread(cell.light, cellLeftUp.light, diagDropoff); cellLeftDown.light = LightTraits::spread(cell.light, cellLeftDown.light, diagDropoff); } } } } template float CellularLightArray::lineAttenuation(Vec2F const& start, Vec2F const& end, float perObstacleAttenuation, float maxAttenuation) { // Run Xiaolin Wu's line algorithm from start to end, summing over colliding // blocks using perObstacleAttenuation. float obstacleAttenuation = 0.0; // Apply correction because integer coordinates are lower left corner. float x1 = start[0] - 0.5; float y1 = start[1] - 0.5; float x2 = end[0] - 0.5; float y2 = end[1] - 0.5; float dx = x2 - x1; float dy = y2 - y1; if (fabs(dx) < fabs(dy)) { if (y2 < y1) { swap(y1, y2); swap(x1, x2); } float gradient = dx / dy; // first end point float yend = round(y1); float xend = x1 + gradient * (yend - y1); float ygap = rfpart(y1 + 0.5); int ypxl1 = yend; int xpxl1 = ipart(xend); if (cell(xpxl1, ypxl1).obstacle) obstacleAttenuation += rfpart(xend) * ygap * perObstacleAttenuation; if (cell(xpxl1 + 1, ypxl1).obstacle) obstacleAttenuation += fpart(xend) * ygap * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; float interx = xend + gradient; // second end point yend = round(y2); xend = x2 + gradient * (yend - y2); ygap = fpart(y2 + 0.5); int ypxl2 = yend; int xpxl2 = ipart(xend); if (cell(xpxl2, ypxl2).obstacle) obstacleAttenuation += rfpart(xend) * ygap * perObstacleAttenuation; if (cell(xpxl2 + 1, ypxl2).obstacle) obstacleAttenuation += fpart(xend) * ygap * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; for (int y = ypxl1 + 1; y < ypxl2; ++y) { int interxIpart = ipart(interx); float interxFpart = interx - interxIpart; float interxRFpart = 1.0 - interxFpart; if (cell(interxIpart, y).obstacle) obstacleAttenuation += interxRFpart * perObstacleAttenuation; if (cell(interxIpart + 1, y).obstacle) obstacleAttenuation += interxFpart * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; interx += gradient; } } else { if (x2 < x1) { swap(x1, x2); swap(y1, y2); } float gradient = dy / dx; // first end point float xend = round(x1); float yend = y1 + gradient * (xend - x1); float xgap = rfpart(x1 + 0.5); int xpxl1 = xend; int ypxl1 = ipart(yend); if (cell(xpxl1, ypxl1).obstacle) obstacleAttenuation += rfpart(yend) * xgap * perObstacleAttenuation; if (cell(xpxl1, ypxl1 + 1).obstacle) obstacleAttenuation += fpart(yend) * xgap * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; float intery = yend + gradient; // second end point xend = round(x2); yend = y2 + gradient * (xend - x2); xgap = fpart(x2 + 0.5); int xpxl2 = xend; int ypxl2 = ipart(yend); if (cell(xpxl2, ypxl2).obstacle) obstacleAttenuation += rfpart(yend) * xgap * perObstacleAttenuation; if (cell(xpxl2, ypxl2 + 1).obstacle) obstacleAttenuation += fpart(yend) * xgap * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; for (int x = xpxl1 + 1; x < xpxl2; ++x) { int interyIpart = ipart(intery); float interyFpart = intery - interyIpart; float interyRFpart = 1.0 - interyFpart; if (cell(x, interyIpart).obstacle) obstacleAttenuation += interyRFpart * perObstacleAttenuation; if (cell(x, interyIpart + 1).obstacle) obstacleAttenuation += interyFpart * perObstacleAttenuation; if (obstacleAttenuation >= maxAttenuation) return maxAttenuation; intery += gradient; } } return min(obstacleAttenuation, maxAttenuation); } }