Files
EVEngine/src/modules/physics/Fluid2D.cpp
T

341 lines
11 KiB
C++

#include "physics/Fluid2D.h"
#include "common/Exception.h"
#include "graphics/Graphics.h"
#include "graphics/Canvas.h"
#include <algorithm>
#include <cmath>
namespace eve::physics {
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
using eve::graphics::Color;
Fluid2D::Fluid2D(int capacity) : capacity_(capacity) {
if (capacity_ < 1) throw Exception("Fluid2D: capacity must be >= 1");
particles_.reserve(static_cast<size_t>(capacity_));
}
Fluid2D::~Fluid2D() { destroy(); }
void Fluid2D::destroy() {
destroyed_ = true;
particles_.clear();
hash_.clear();
}
void Fluid2D::setGravity(float gx, float gy) {
gravityX_ = gx;
gravityY_ = gy;
}
void Fluid2D::setSmoothingRadius(float radius) {
if (radius <= 0.f) throw Exception("Fluid2D.setSmoothingRadius: radius must be > 0");
h_ = radius;
}
void Fluid2D::setRestDensity(float density) {
if (density <= 0.f) throw Exception("Fluid2D.setRestDensity: density must be > 0");
restDensity_ = density;
}
void Fluid2D::setPressureStiffness(float k) { pressureK_ = std::max(0.f, k); }
void Fluid2D::setNearPressureStiffness(float k) { nearPressureK_ = std::max(0.f, k); }
void Fluid2D::setViscosity(float viscosity) { viscosity_ = std::clamp(viscosity, 0.f, 1.f); }
void Fluid2D::setIterations(int iterations) { iterations_ = std::max(1, iterations); }
void Fluid2D::setBounds(float x, float y, float w, float h) {
if (w <= 0.f || h <= 0.f) {
clearBounds();
return;
}
hasBounds_ = true;
boundX_ = x;
boundY_ = y;
boundW_ = w;
boundH_ = h;
}
void Fluid2D::clearBounds() { hasBounds_ = false; }
int Fluid2D::emit(float x, float y, int count, float vx, float vy) {
if (destroyed_ || count <= 0) return 0;
int added = 0;
const float step = std::max(2.5f, h_ * 0.35f);
const int side = std::max(1, int(std::ceil(std::sqrt(float(count)))));
for (int i = 0; i < count && getParticleCount() < capacity_; ++i) {
Particle p;
const int cx = i % side;
const int cy = i / side;
p.x = x + (float(cx) - float(side) * 0.5f) * step;
p.y = y + (float(cy) - float(side) * 0.5f) * step;
p.vx = vx;
p.vy = vy;
particles_.push_back(p);
++added;
}
return added;
}
void Fluid2D::clear() {
particles_.clear();
hash_.clear();
}
void Fluid2D::interactAt(float x, float y, float radius, float strength) {
interactX_ = x;
interactY_ = y;
interactRadius_ = std::max(0.f, radius);
interactStrength_ = strength;
}
void Fluid2D::setColor(float r, float g, float b, float a) {
colorR_ = r;
colorG_ = g;
colorB_ = b;
colorA_ = a;
}
void Fluid2D::setParticleSize(float size) { particleSize_ = std::max(1.f, size); }
bool Fluid2D::validIndex(int index) const {
return index >= 0 && index < getParticleCount();
}
float Fluid2D::getParticleX(int index) const {
if (!validIndex(index)) return 0.f;
return particles_[static_cast<size_t>(index)].x;
}
float Fluid2D::getParticleY(int index) const {
if (!validIndex(index)) return 0.f;
return particles_[static_cast<size_t>(index)].y;
}
float Fluid2D::getParticleVx(int index) const {
if (!validIndex(index)) return 0.f;
return particles_[static_cast<size_t>(index)].vx;
}
float Fluid2D::getParticleVy(int index) const {
if (!validIndex(index)) return 0.f;
return particles_[static_cast<size_t>(index)].vy;
}
int64_t Fluid2D::cellKey(int cx, int cy) const {
return (int64_t(uint32_t(cx)) << 32) | int64_t(uint32_t(cy));
}
void Fluid2D::rebuildHash() {
hash_.clear();
const float inv = 1.f / h_;
for (int i = 0; i < getParticleCount(); ++i) {
const Particle &p = particles_[static_cast<size_t>(i)];
const int cx = int(std::floor(p.x * inv));
const int cy = int(std::floor(p.y * inv));
hash_[cellKey(cx, cy)].push_back(i);
}
}
void Fluid2D::applyViscosity(float dt) {
if (viscosity_ <= 0.f || getParticleCount() == 0) return;
const float inv = 1.f / h_;
for (int i = 0; i < getParticleCount(); ++i) {
Particle &pi = particles_[static_cast<size_t>(i)];
const int cx = int(std::floor(pi.x * inv));
const int cy = int(std::floor(pi.y * inv));
for (int oy = -1; oy <= 1; ++oy) {
for (int ox = -1; ox <= 1; ++ox) {
auto it = hash_.find(cellKey(cx + ox, cy + oy));
if (it == hash_.end()) continue;
for (int j : it->second) {
if (j <= i) continue;
Particle &pj = particles_[static_cast<size_t>(j)];
float dx = pj.x - pi.x;
float dy = pj.y - pi.y;
float r2 = dx * dx + dy * dy;
if (r2 >= h_ * h_ || r2 < 1e-8f) continue;
const float r = std::sqrt(r2);
const float q = 1.f - r / h_;
float dvx = pj.vx - pi.vx;
float dvy = pj.vy - pi.vy;
const float impulse = viscosity_ * q * dt;
pi.vx += dvx * impulse * 0.5f;
pi.vy += dvy * impulse * 0.5f;
pj.vx -= dvx * impulse * 0.5f;
pj.vy -= dvy * impulse * 0.5f;
}
}
}
}
}
void Fluid2D::doubleDensityRelaxation() {
const float inv = 1.f / h_;
for (int i = 0; i < getParticleCount(); ++i) {
Particle &pi = particles_[static_cast<size_t>(i)];
float density = 0.f;
float nearDensity = 0.f;
const int cx = int(std::floor(pi.x * inv));
const int cy = int(std::floor(pi.y * inv));
struct Neighbor {
int j;
float r;
float dx;
float dy;
float q;
};
Neighbor neighbors[64];
int nCount = 0;
for (int oy = -1; oy <= 1; ++oy) {
for (int ox = -1; ox <= 1; ++ox) {
auto it = hash_.find(cellKey(cx + ox, cy + oy));
if (it == hash_.end()) continue;
for (int j : it->second) {
if (j == i) continue;
const Particle &pj = particles_[static_cast<size_t>(j)];
float dx = pj.x - pi.x;
float dy = pj.y - pi.y;
float r2 = dx * dx + dy * dy;
if (r2 >= h_ * h_ || r2 < 1e-10f) continue;
const float r = std::sqrt(r2);
const float q = 1.f - r / h_;
density += q * q;
nearDensity += q * q * q;
if (nCount < 64) neighbors[nCount++] = {j, r, dx, dy, q};
}
}
}
pi.density = density;
const float pressure = pressureK_ * (density - restDensity_);
const float nearPressure = nearPressureK_ * nearDensity;
float dxSum = 0.f;
float dySum = 0.f;
for (int n = 0; n < nCount; ++n) {
const Neighbor &nb = neighbors[n];
const float mag =
(pressure * nb.q + nearPressure * nb.q * nb.q) * (1.f / (nb.r + 1e-6f));
const float dispX = nb.dx * mag * 0.5f;
const float dispY = nb.dy * mag * 0.5f;
Particle &pj = particles_[static_cast<size_t>(nb.j)];
pj.x += dispX;
pj.y += dispY;
dxSum -= dispX;
dySum -= dispY;
}
pi.x += dxSum;
pi.y += dySum;
}
}
void Fluid2D::collideBounds() {
if (!hasBounds_) return;
const float pad = particleSize_ * 0.5f;
const float minX = boundX_ + pad;
const float minY = boundY_ + pad;
const float maxX = boundX_ + boundW_ - pad;
const float maxY = boundY_ + boundH_ - pad;
constexpr float damp = 0.35f;
for (Particle &p : particles_) {
if (p.x < minX) {
p.x = minX;
p.vx = std::fabs(p.vx) * damp;
} else if (p.x > maxX) {
p.x = maxX;
p.vx = -std::fabs(p.vx) * damp;
}
if (p.y < minY) {
p.y = minY;
p.vy = std::fabs(p.vy) * damp;
} else if (p.y > maxY) {
p.y = maxY;
p.vy = -std::fabs(p.vy) * damp;
}
}
}
void Fluid2D::update(float dt) {
if (destroyed_) return;
if (getParticleCount() == 0) {
interactStrength_ = 0.f;
return;
}
if (dt < 0.f) dt = 0.f;
if (dt > 0.05f) dt = 0.05f;
// External accelerations.
for (Particle &p : particles_) {
p.vx += gravityX_ * dt;
p.vy += gravityY_ * dt;
if (interactRadius_ > 0.f && interactStrength_ != 0.f) {
const float dx = interactX_ - p.x;
const float dy = interactY_ - p.y;
const float r2 = dx * dx + dy * dy;
const float R2 = interactRadius_ * interactRadius_;
if (r2 < R2 && r2 > 1e-6f) {
const float r = std::sqrt(r2);
const float w = 1.f - r / interactRadius_;
p.vx += (dx / r) * interactStrength_ * w * dt;
p.vy += (dy / r) * interactStrength_ * w * dt;
}
}
}
rebuildHash();
applyViscosity(dt);
// Predict positions, then relax density.
std::vector<float> prevX(particles_.size()), prevY(particles_.size());
for (size_t i = 0; i < particles_.size(); ++i) {
prevX[i] = particles_[i].x;
prevY[i] = particles_[i].y;
particles_[i].x += particles_[i].vx * dt;
particles_[i].y += particles_[i].vy * dt;
}
for (int iter = 0; iter < iterations_; ++iter) {
rebuildHash();
doubleDensityRelaxation();
collideBounds();
}
// Update velocities from position delta.
const float invDt = dt > 1e-6f ? 1.f / dt : 0.f;
for (size_t i = 0; i < particles_.size(); ++i) {
particles_[i].vx = (particles_[i].x - prevX[i]) * invDt;
particles_[i].vy = (particles_[i].y - prevY[i]) * invDt;
const float speed2 =
particles_[i].vx * particles_[i].vx + particles_[i].vy * particles_[i].vy;
const float maxSpeed = 1600.f;
if (speed2 > maxSpeed * maxSpeed) {
const float s = maxSpeed / std::sqrt(speed2);
particles_[i].vx *= s;
particles_[i].vy *= s;
}
}
interactStrength_ = 0.f;
}
void Fluid2D::draw(graphics::Graphics *gfx) {
if (!gfx || destroyed_) return;
const float s = particleSize_;
for (const Particle &p : particles_) {
const float t = std::clamp(p.density / (restDensity_ * 1.8f), 0.25f, 1.f);
gfx->drawSolidRect(p.x - s * 0.5f, p.y - s * 0.5f, s, s,
Color(colorR_ * (0.55f + 0.45f * t), colorG_ * (0.65f + 0.35f * t),
colorB_, colorA_));
}
}
} // namespace eve::physics