osb/source/base/StarCellularLiquid.hpp
2023-06-20 14:33:09 +10:00

659 lines
21 KiB
C++

#ifndef STAR_CELLULAR_LIQUID_HPP
#define STAR_CELLULAR_LIQUID_HPP
#include "StarVariant.hpp"
#include "StarRect.hpp"
#include "StarMultiArray.hpp"
#include "StarMap.hpp"
#include "StarOrderedSet.hpp"
#include "StarRandom.hpp"
#include "StarBlockAllocator.hpp"
namespace Star {
struct CellularLiquidCollisionCell {};
template <typename LiquidId>
struct CellularLiquidFlowCell {
Maybe<LiquidId> liquid;
float level;
float pressure;
};
template <typename LiquidId>
struct CellularLiquidSourceCell {
LiquidId liquid;
float pressure;
};
template <typename LiquidId>
using CellularLiquidCell = Variant<CellularLiquidCollisionCell, CellularLiquidFlowCell<LiquidId>, CellularLiquidSourceCell<LiquidId>>;
template <typename LiquidId>
struct CellularLiquidWorld {
virtual ~CellularLiquidWorld();
virtual Vec2I uniqueLocation(Vec2I const& location) const;
virtual CellularLiquidCell<LiquidId> cell(Vec2I const& location) const = 0;
// Should return an amount between 0.0 and 1.0 as a percentage of liquid
// drain at this position
virtual float drainLevel(Vec2I const& location) const;
// Will be called only on cells which for which the cell method returned a
// flow cell, to update the flow cell.
virtual void setFlow(Vec2I const& location, CellularLiquidFlowCell<LiquidId> const& flow) = 0;
// Called once for every active liquid <-> liquid interaction of different
// liquid types each update. Will be called AFTER pushing all the flow
// values back out so modifications to liquids are sensible.
virtual void liquidInteraction(Vec2I const& a, LiquidId aLiquid, Vec2I const& b, LiquidId bLiquid);
// Called once for every liquid collision each update. Also called after
// pushing all the flow values out, so changes to liquids can sensibly be
// performed here.
virtual void liquidCollision(Vec2I const& pos, LiquidId liquid, Vec2I const& collisionPos);
};
struct LiquidCellEngineParameters {
float lateralMoveFactor;
float spreadOverfillUpFactor;
float spreadOverfillLateralFactor;
float spreadOverfillDownFactor;
float pressureEqualizeFactor;
float pressureMoveFactor;
float maximumPressureLevelImbalance;
float minimumLivenPressureChange;
float minimumLivenLevelChange;
float minimumLiquidLevel;
float interactTransformationLevel;
};
template <typename LiquidId>
class LiquidCellEngine {
public:
typedef shared_ptr<CellularLiquidWorld<LiquidId>> CellularLiquidWorldPtr;
LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld);
unsigned liquidTickDelta(LiquidId liquid);
void setLiquidTickDelta(LiquidId liquid, unsigned tickDelta);
void setProcessingLimit(Maybe<unsigned> processingLimit);
List<RectI> noProcessingLimitRegions() const;
void setNoProcessingLimitRegions(List<RectI> noProcessingLimitRegions);
void visitLocation(Vec2I const& location);
void visitRegion(RectI const& region);
void update();
size_t activeCells() const;
size_t activeCells(LiquidId liquid) const;
bool isActive(Vec2I const& pos) const;
private:
enum class Adjacency {
Left,
Right,
Bottom,
Top
};
struct WorkingCell {
Vec2I position;
Maybe<LiquidId> liquid;
bool sourceCell;
float level;
float pressure;
WorkingCell* leftCell;
WorkingCell* rightCell;
WorkingCell* topCell;
WorkingCell* bottomCell;
};
template <typename Key, typename Value>
using BAHashMap = StableHashMap<Key, Value, hash<Key>, std::equal_to<Key>, BlockAllocator<pair<Key const, Value>, 4096>>;
template <typename Value>
using BAHashSet = HashSet<Value, hash<Value>, std::equal_to<Value>>;
template <typename Value>
using BAOrderedHashSet = OrderedHashSet<Value, hash<Value>, std::equal_to<Value>, BlockAllocator<Value, 4096>>;
void setup();
void applyPressure();
void spreadPressure();
void limitPressure();
void pressureMove();
void spreadOverfill();
void levelMove();
void findInteractions();
void finish();
WorkingCell* workingCell(Vec2I p);
WorkingCell* adjacentCell(WorkingCell* cell, Adjacency adjacency);
void setPressure(float pressure, WorkingCell& cell);
void transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse);
void transferLevel(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse);
void setLevel(float level, WorkingCell& cell);
RandomSource m_random;
LiquidCellEngineParameters m_engineParameters;
CellularLiquidWorldPtr m_cellWorld;
BAHashMap<LiquidId, BAOrderedHashSet<Vec2I>> m_activeCells;
BAHashMap<LiquidId, unsigned> m_liquidTickDeltas;
Maybe<unsigned> m_processingLimit;
List<RectI> m_noProcessingLimitRegions;
uint64_t m_step;
BAHashMap<Vec2I, Maybe<WorkingCell>> m_workingCells;
List<WorkingCell*> m_currentActiveCells;
BAHashSet<Vec2I> m_nextActiveCells;
BAHashSet<tuple<Vec2I, LiquidId, Vec2I, LiquidId>> m_liquidInteractions;
BAHashSet<tuple<Vec2I, LiquidId, Vec2I>> m_liquidCollisions;
};
template <typename LiquidId>
CellularLiquidWorld<LiquidId>::~CellularLiquidWorld() {}
template <typename LiquidId>
Vec2I CellularLiquidWorld<LiquidId>::uniqueLocation(Vec2I const& location) const {
return location;
}
template <typename LiquidId>
float CellularLiquidWorld<LiquidId>::drainLevel(Vec2I const&) const {
return 0.0f;
}
template <typename LiquidId>
void CellularLiquidWorld<LiquidId>::liquidInteraction(Vec2I const&, LiquidId, Vec2I const&, LiquidId) {}
template <typename LiquidId>
void CellularLiquidWorld<LiquidId>::liquidCollision(Vec2I const&, LiquidId, Vec2I const&) {}
template <typename LiquidId>
LiquidCellEngine<LiquidId>::LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld)
: m_engineParameters(parameters), m_cellWorld(cellWorld), m_step(0) {}
template <typename LiquidId>
unsigned LiquidCellEngine<LiquidId>::liquidTickDelta(LiquidId liquid) {
return m_liquidTickDeltas.value(liquid, 1);
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setLiquidTickDelta(LiquidId liquid, unsigned tickDelta) {
m_liquidTickDeltas[liquid] = tickDelta;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setProcessingLimit(Maybe<unsigned> processingLimit) {
m_processingLimit = processingLimit;
}
template <typename LiquidId>
List<RectI> LiquidCellEngine<LiquidId>::noProcessingLimitRegions() const {
return m_noProcessingLimitRegions;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setNoProcessingLimitRegions(List<RectI> noProcessingLimitRegions) {
m_noProcessingLimitRegions = noProcessingLimitRegions;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::visitLocation(Vec2I const& p) {
m_nextActiveCells.add(p);
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::visitRegion(RectI const& region) {
for (int x = region.xMin(); x < region.xMax(); ++x) {
for (int y = region.yMin(); y < region.yMax(); ++y)
m_nextActiveCells.add({x, y});
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::update() {
setup();
applyPressure();
spreadPressure();
limitPressure();
pressureMove();
spreadOverfill();
levelMove();
findInteractions();
finish();
++m_step;
}
template <typename LiquidId>
size_t LiquidCellEngine<LiquidId>::activeCells() const {
size_t totalSize = 0;
for (auto const& p : m_activeCells)
totalSize += p.second.size();
return totalSize;
}
template <typename LiquidId>
size_t LiquidCellEngine<LiquidId>::activeCells(LiquidId liquid) const {
return m_activeCells.value(liquid).size();
}
template <typename LiquidId>
bool LiquidCellEngine<LiquidId>::isActive(Vec2I const& pos) const {
for (auto const& p : m_activeCells) {
if (p.second.contains(pos))
return true;
}
return false;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setup() {
// In case an exception occurred during the last update, clear potentially
// stale data here
m_workingCells.clear();
m_currentActiveCells.clear();
for (auto& activeCellsPair : m_activeCells) {
unsigned tickDelta = liquidTickDelta(activeCellsPair.first);
if (tickDelta == 0 || m_step % tickDelta != 0)
continue;
size_t limitedCellNumber = 0;
for (auto const& pos : activeCellsPair.second.values()) {
if (m_processingLimit) {
bool foundInUnlimitedRegion = false;
for (auto const& region : m_noProcessingLimitRegions) {
if (region.contains(pos)) {
foundInUnlimitedRegion = true;
break;
}
}
if (!foundInUnlimitedRegion) {
if (limitedCellNumber < *m_processingLimit)
++limitedCellNumber;
else
continue;
}
}
auto cell = workingCell(pos);
if (!cell || cell->liquid != activeCellsPair.first) {
activeCellsPair.second.remove(pos);
} else {
m_currentActiveCells.append(cell);
activeCellsPair.second.remove(pos);
}
}
}
sort(m_currentActiveCells, [](WorkingCell* lhs, WorkingCell* rhs) {
return lhs->position[1] < rhs->position[1];
});
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::applyPressure() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid || selfCell->sourceCell)
continue;
auto topCell = adjacentCell(selfCell, Adjacency::Top);
if (topCell && selfCell->liquid == topCell->liquid)
setPressure(max(selfCell->pressure, topCell->pressure + min(topCell->level, 1.0f)), *selfCell);
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::spreadPressure() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid)
continue;
auto spreadPressure = [&](Adjacency adjacency, float bias) {
auto targetCell = adjacentCell(selfCell, adjacency);
if (targetCell && !targetCell->sourceCell)
transferPressure((selfCell->pressure + bias - targetCell->pressure) * m_engineParameters.pressureEqualizeFactor, *selfCell, *targetCell, true);
};
if (m_random.randb()) {
spreadPressure(Adjacency::Left, 0.0f);
spreadPressure(Adjacency::Right, 0.0f);
} else {
spreadPressure(Adjacency::Right, 0.0f);
spreadPressure(Adjacency::Left, 0.0f);
}
spreadPressure(Adjacency::Bottom, 1.0f);
spreadPressure(Adjacency::Top, -1.0f);
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::limitPressure() {
for (auto const& selfCell : m_currentActiveCells) {
float level = min(selfCell->level, 1.0f);
auto topCell = adjacentCell(selfCell, Adjacency::Top);
// Force the pressure to the cell level if there is empty space above,
// otherwise simply make sure the pressure is at least the level
if (topCell && !topCell->liquid)
setPressure(level, *selfCell);
else
setPressure(max(selfCell->pressure, level), *selfCell);
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::pressureMove() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid)
continue;
auto pressureMove = [&](Adjacency adjacency) {
auto targetCell = adjacentCell(selfCell, adjacency);
if (targetCell && !targetCell->sourceCell && targetCell->level >= selfCell->level) {
float amount = (selfCell->pressure - targetCell->pressure) * m_engineParameters.pressureMoveFactor;
amount = min(amount, selfCell->level - (1.0f - m_engineParameters.maximumPressureLevelImbalance));
amount = min(amount, (1.0f + m_engineParameters.maximumPressureLevelImbalance) - targetCell->level);
transferLevel(amount, *selfCell, *targetCell, false);
}
};
if (m_random.randb()) {
pressureMove(Adjacency::Left);
pressureMove(Adjacency::Right);
} else {
pressureMove(Adjacency::Right);
pressureMove(Adjacency::Left);
}
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::spreadOverfill() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid || selfCell->sourceCell)
continue;
auto spreadOverfill = [&](Adjacency adjacency, float factor) {
float overfill = selfCell->level - 1.0f;
if (overfill > 0.0f) {
auto targetCell = adjacentCell(selfCell, adjacency);
if (targetCell)
transferLevel(min(overfill, (selfCell->level - targetCell->level)) * factor, *selfCell, *targetCell, false);
}
};
spreadOverfill(Adjacency::Top, m_engineParameters.spreadOverfillUpFactor);
if (m_random.randb()) {
spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor);
spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor);
} else {
spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor);
spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor);
}
spreadOverfill(Adjacency::Bottom, m_engineParameters.spreadOverfillDownFactor);
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::levelMove() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid)
continue;
auto belowCell = adjacentCell(selfCell, Adjacency::Bottom);
if (belowCell)
transferLevel(min(1.0f - belowCell->level, selfCell->level), *selfCell, *belowCell, false);
setLevel(selfCell->level * (1.0f - m_cellWorld->drainLevel(selfCell->position)), *selfCell);
auto lateralMove = [&](Adjacency adjacency) {
auto targetCell = adjacentCell(selfCell, adjacency);
if (targetCell)
transferLevel((selfCell->level - targetCell->level) * m_engineParameters.lateralMoveFactor, *selfCell, *targetCell, false);
};
if (m_random.randb()) {
lateralMove(Adjacency::Left);
lateralMove(Adjacency::Right);
} else {
lateralMove(Adjacency::Right);
lateralMove(Adjacency::Left);
}
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::findInteractions() {
for (auto const& selfCell : m_currentActiveCells) {
if (!selfCell->liquid)
continue;
for (auto adjacency : {Adjacency::Bottom, Adjacency::Top, Adjacency::Left, Adjacency::Right}) {
auto targetCell = adjacentCell(selfCell, adjacency);
if (!targetCell) {
Vec2I adjacentPos = selfCell->position;
if (adjacency == Adjacency::Left)
adjacentPos += Vec2I(-1, 0);
else if (adjacency == Adjacency::Right)
adjacentPos += Vec2I(1, 0);
else if (adjacency == Adjacency::Bottom)
adjacentPos += Vec2I(0, -1);
else if (adjacency == Adjacency::Top)
adjacentPos += Vec2I(0, 1);
m_liquidCollisions.add(make_tuple(selfCell->position, *selfCell->liquid, adjacentPos));
} else if (targetCell->liquid && *targetCell->liquid != *selfCell->liquid) {
if (targetCell->level <= m_engineParameters.interactTransformationLevel
|| selfCell->level <= m_engineParameters.interactTransformationLevel) {
if (selfCell->level > targetCell->level)
targetCell->liquid = selfCell->liquid;
else
selfCell->liquid = targetCell->liquid;
} else {
// Make sure to add the point pair in a predictable order so that any
// combination of Vec2I points will be unique in m_liquidInteractions
if (selfCell->position < targetCell->position)
m_liquidInteractions.add(make_tuple(selfCell->position, *selfCell->liquid, targetCell->position, *targetCell->liquid));
else
m_liquidInteractions.add(make_tuple(targetCell->position, *targetCell->liquid, selfCell->position, *selfCell->liquid));
}
}
}
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::finish() {
m_currentActiveCells.clear();
for (auto& workingCellPair : take(m_workingCells)) {
if (workingCellPair.second && !workingCellPair.second->sourceCell) {
if (workingCellPair.second->liquid) {
if (workingCellPair.second->level < m_engineParameters.minimumLiquidLevel)
workingCellPair.second->level = 0.0f;
} else {
workingCellPair.second->level = 0.0f;
}
if (workingCellPair.second->level == 0.0f) {
workingCellPair.second->liquid = {};
workingCellPair.second->pressure = 0.0f;
}
m_cellWorld->setFlow(workingCellPair.second->position, CellularLiquidFlowCell<LiquidId>{
workingCellPair.second->liquid, workingCellPair.second->level, workingCellPair.second->pressure});
}
}
for (auto const& interaction : take(m_liquidInteractions))
m_cellWorld->liquidInteraction(get<0>(interaction), get<1>(interaction), get<2>(interaction), get<3>(interaction));
for (auto const& interaction : take(m_liquidCollisions))
m_cellWorld->liquidCollision(get<0>(interaction), get<1>(interaction), get<2>(interaction));
for (auto const& c : take(m_nextActiveCells)) {
auto visit = [this](Vec2I p) {
p = m_cellWorld->uniqueLocation(p);
auto cell = workingCell(p);
if (cell && cell->liquid)
m_activeCells[*cell->liquid].add(p);
};
visit(c);
visit(c + Vec2I(-1, 0));
visit(c + Vec2I(1, 0));
visit(c + Vec2I(0, -1));
visit(c + Vec2I(0, 1));
}
eraseWhere(m_activeCells, [](auto const& p) {
return p.second.empty();
});
}
template <typename LiquidId>
typename LiquidCellEngine<LiquidId>::WorkingCell* LiquidCellEngine<LiquidId>::workingCell(Vec2I p) {
p = m_cellWorld->uniqueLocation(p);
auto res = m_workingCells.insert(make_pair(p, Maybe<WorkingCell>()));
if (res.second) {
auto cellData = m_cellWorld->cell(p);
if (auto flowCell = cellData.template ptr<CellularLiquidFlowCell<LiquidId>>())
res.first->second = WorkingCell{p, flowCell->liquid, false, flowCell->level, flowCell->pressure, nullptr, nullptr, nullptr, nullptr};
else if (auto sourceCell = cellData.template ptr<CellularLiquidSourceCell<LiquidId>>())
res.first->second = WorkingCell{p, sourceCell->liquid, true, 1.0f, sourceCell->pressure, nullptr, nullptr, nullptr, nullptr};
}
return res.first->second.ptr();
}
template <typename LiquidId>
typename LiquidCellEngine<LiquidId>::WorkingCell* LiquidCellEngine<LiquidId>::adjacentCell(
WorkingCell* cell, Adjacency adjacency) {
auto getCell = [this](WorkingCell*& cellptr, Vec2I cellPos) {
if (cellptr)
return cellptr;
cellptr = workingCell(cellPos);
return cellptr;
};
if (adjacency == Adjacency::Left)
return getCell(cell->leftCell, cell->position + Vec2I(-1, 0));
else if (adjacency == Adjacency::Right)
return getCell(cell->rightCell, cell->position + Vec2I(1, 0));
else if (adjacency == Adjacency::Bottom)
return getCell(cell->bottomCell, cell->position + Vec2I(0, -1));
else if (adjacency == Adjacency::Top)
return getCell(cell->topCell, cell->position + Vec2I(0, 1));
return nullptr;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setPressure(float pressure, WorkingCell& cell) {
if (!cell.liquid || cell.sourceCell)
return;
if (fabs(cell.pressure - pressure) > m_engineParameters.minimumLivenPressureChange)
m_nextActiveCells.add(cell.position);
cell.pressure = pressure;
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) {
if (amount < 0.0f && allowReverse) {
return transferPressure(-amount, dest, source, false);
} else if (amount > 0.0f) {
if (!source.liquid)
return;
if (source.sourceCell && dest.sourceCell)
return;
if (dest.liquid && dest.liquid != source.liquid)
return;
amount = min(amount, source.pressure);
if (!source.sourceCell)
source.pressure -= amount;
if (dest.liquid && !dest.sourceCell)
dest.pressure += amount;
if (amount > m_engineParameters.minimumLivenPressureChange) {
m_nextActiveCells.add(source.position);
m_nextActiveCells.add(dest.position);
}
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::setLevel(float level, WorkingCell& cell) {
if (!cell.liquid || cell.sourceCell)
return;
if (fabs(cell.level - level) > m_engineParameters.minimumLivenLevelChange)
m_nextActiveCells.add(cell.position);
cell.level = level;
if (cell.level <= 0.0f) {
cell.liquid = {};
cell.level = 0.0f;
}
}
template <typename LiquidId>
void LiquidCellEngine<LiquidId>::transferLevel(
float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) {
if (amount < 0.0f && allowReverse) {
transferLevel(-amount, dest, source, false);
} else if (amount > 0.0f) {
if (!source.liquid)
return;
if (source.sourceCell && dest.sourceCell)
return;
if (dest.liquid && dest.liquid != source.liquid)
return;
amount = min(amount, source.level);
if (!source.sourceCell)
source.level -= amount;
if (!dest.sourceCell) {
dest.level += amount;
dest.liquid = source.liquid;
}
if (!source.sourceCell && source.level == 0.0f)
source.liquid = {};
if (amount > m_engineParameters.minimumLivenLevelChange) {
m_nextActiveCells.add(source.position);
m_nextActiveCells.add(dest.position);
}
}
}
}
#endif