osb/source/base/StarCellularLightArray.hpp

536 lines
18 KiB
C++
Raw Normal View History

#pragma once
2023-06-20 04:33:09 +00:00
#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);
2024-03-19 14:53:34 +00:00
static float multiply(float v1, float v2);
2023-06-20 04:33:09 +00:00
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);
2024-03-19 14:53:34 +00:00
static Vec3F multiply(Vec3F value, float drop);
2023-06-20 04:33:09 +00:00
static float maxIntensity(Vec3F const& value);
static float minIntensity(Vec3F const& value);
static Vec3F max(Vec3F const& v1, Vec3F const& v2);
};
template <typename LightTraits>
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;
};
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<Cell[]> m_cells;
List<SpreadLight> m_spreadLights;
List<PointLight> m_pointLights;
unsigned m_spreadPasses;
float m_spreadMaxAir;
float m_spreadMaxObstacle;
float m_pointMaxAir;
float m_pointMaxObstacle;
float m_pointObstacleBoost;
};
typedef CellularLightArray<ColoredLightTraits> ColoredCellularLightArray;
typedef CellularLightArray<ScalarLightTraits> 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);
}
2024-03-19 14:53:34 +00:00
inline float ScalarLightTraits::multiply(float v1, float v2) {
return v1 * v2;
}
2023-06-20 04:33:09 +00:00
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;
}
2024-03-19 14:53:34 +00:00
inline Vec3F ColoredLightTraits::multiply(Vec3F c, float drop) {
return c * drop;
}
2023-06-20 04:33:09 +00:00
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 <typename LightTraits>
void CellularLightArray<LightTraits>::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 <typename LightTraits>
size_t CellularLightArray<LightTraits>::borderCells() const {
return (size_t)ceil(max(0.0f, max(m_spreadMaxAir, m_pointMaxAir)));
}
template <typename LightTraits>
void CellularLightArray<LightTraits>::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 <typename LightTraits>
void CellularLightArray<LightTraits>::addSpreadLight(SpreadLight const& spreadLight) {
m_spreadLights.append(spreadLight);
}
template <typename LightTraits>
void CellularLightArray<LightTraits>::addPointLight(PointLight const& pointLight) {
m_pointLights.append(pointLight);
}
template <typename LightTraits>
void CellularLightArray<LightTraits>::setLight(size_t x, size_t y, LightValue const& lightValue) {
cell(x, y).light = lightValue;
}
template <typename LightTraits>
void CellularLightArray<LightTraits>::setObstacle(size_t x, size_t y, bool obstacle) {
cell(x, y).obstacle = obstacle;
}
template <typename LightTraits>
auto CellularLightArray<LightTraits>::getLight(size_t x, size_t y) const -> LightValue {
return cell(x, y).light;
}
template <typename LightTraits>
bool CellularLightArray<LightTraits>::getObstacle(size_t x, size_t y) const {
return cell(x, y).obstacle;
}
template <typename LightTraits>
auto CellularLightArray<LightTraits>::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 <typename LightTraits>
auto CellularLightArray<LightTraits>::cell(size_t x, size_t y) -> Cell & {
starAssert(x < m_width && y < m_height);
return m_cells[x * m_height + y];
}
template <typename LightTraits>
auto CellularLightArray<LightTraits>::cellAtIndex(size_t index) const -> Cell const & {
starAssert(index < m_width * m_height);
return m_cells[index];
}
template <typename LightTraits>
auto CellularLightArray<LightTraits>::cellAtIndex(size_t index) -> Cell & {
starAssert(index < m_width * m_height);
return m_cells[index];
}
template <typename LightTraits>
void CellularLightArray<LightTraits>::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 <typename LightTraits>
void CellularLightArray<LightTraits>::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 <typename LightTraits>
void CellularLightArray<LightTraits>::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 <typename LightTraits>
float CellularLightArray<LightTraits>::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);
}
}