Files
EVEngine/src/modules/fluids/FluidSimulation.cpp
T

292 lines
12 KiB
C++

#include "fluids/FluidSimulation.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace eve::fluids {
SimGrid SimGrid::make(const MeshSdf& sdf, float cellSize) {
SimGrid grid;
grid.cellSize = std::max(cellSize, 1e-4f);
const glm::vec3 extent = glm::vec3(sdf.dims) * sdf.cellSize;
const glm::vec3 cells = glm::ceil((extent + glm::vec3(2.f * grid.cellSize)) / grid.cellSize);
grid.origin = sdf.origin - glm::vec3(grid.cellSize);
grid.dims = glm::max(glm::ivec3(1), glm::ivec3(cells));
return grid;
}
int SimGrid::cellCount() const { return dims.x * dims.y * dims.z; }
int SimGrid::cellIndex(int x, int y, int z) const { return x + dims.x * (y + dims.y * z); }
bool SimGrid::inBounds(int x, int y, int z) const {
return x >= 0 && y >= 0 && z >= 0 && x < dims.x && y < dims.y && z < dims.z;
}
glm::ivec3 SimGrid::cellOf(const glm::vec3& p) const {
glm::ivec3 c = glm::ivec3(glm::floor((p - origin) / cellSize));
return glm::clamp(c, glm::ivec3(0), dims - glm::ivec3(1));
}
FluidSimulation::FluidSimulation(int maxParticles, const FluidParams& params)
: params_(params), maxParticles_(std::max(1, maxParticles)) {
particles_.resize(size_t(maxParticles_));
densities_.resize(size_t(maxParticles_));
lambdas_.resize(size_t(maxParticles_));
gradSums_.resize(size_t(maxParticles_));
cellNext_.resize(size_t(maxParticles_));
}
void FluidSimulation::setSdf(const MeshSdf& sdf) {
sdf_ = sdf;
grid_ = SimGrid::make(sdf_, params_.supportRadius);
cellHead_.assign(size_t(grid_.cellCount()), -1);
}
void FluidSimulation::clear() {
count_ = 0;
std::fill(cellHead_.begin(), cellHead_.end(), -1);
}
int FluidSimulation::spawnDrop(const glm::vec3& center, float radius, int count) {
int added = 0;
for (int k = 0; k < count && count_ < maxParticles_; ++k) {
// Deterministic golden-ratio sphere fill so tests are stable.
const float z = (float((0x9E3779B9u * uint32_t(k + 1)) % 10000u) / 10000.f) * 2.f - 1.f;
const float a = 2.399963229728653f * float(k);
const float rr = radius * std::sqrt(std::max(0.f, 1.f - z * z));
glm::vec3 p = center + glm::vec3(std::cos(a) * rr, radius * z, std::sin(a) * rr);
if (sdf_.voxelCount() > 0) {
const float d = sdf_.sample(p);
if (d < params_.particleRadius) {
glm::vec3 n = sdf_.gradient(p);
const float nl = glm::length(n);
if (nl > 1e-6f)
n /= nl;
else
n = glm::vec3(0.f, 1.f, 0.f);
p = p - n * (d - params_.particleRadius);
}
}
particles_[size_t(count_)] = FluidParticle{p, glm::vec3(0.f)};
densities_[size_t(count_)] = 0.f;
++count_;
++added;
}
return added;
}
void FluidSimulation::step(float dt) { step(dt, std::max(1, params_.iterations)); }
void FluidSimulation::step(float dt, int substeps) {
if (count_ <= 0 || sdf_.voxelCount() <= 0) return;
const int iters = std::max(1, substeps);
const float sub = dt / float(iters);
for (int i = 0; i < iters; ++i) {
rebuildGrid();
integrate(sub);
const int pbf = std::max(0, params_.pbfIterations);
for (int k = 0; k < pbf; ++k) {
rebuildGrid();
computeDensitiesAndGrads();
computeLambdas();
applyPositionCorrections();
}
}
}
void FluidSimulation::rebuildGrid() {
if (cellHead_.empty()) return;
std::fill(cellHead_.begin(), cellHead_.end(), -1);
for (int i = 0; i < count_; ++i) {
const glm::ivec3 c = grid_.cellOf(particles_[size_t(i)].pos);
const int cell = grid_.cellIndex(c.x, c.y, c.z);
cellNext_[size_t(i)] = cellHead_[size_t(cell)];
cellHead_[size_t(cell)] = i;
}
}
void FluidSimulation::computeDensitiesAndGrads() {
const float h = params_.supportRadius;
const float h2 = h * h;
for (int i = 0; i < count_; ++i) {
const glm::vec3 pi = particles_[size_t(i)].pos;
const glm::ivec3 c = grid_.cellOf(pi);
float dens = 0.f;
glm::vec3 gradSum(0.f);
for (int z = -1; z <= 1; ++z) {
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const int cx = c.x + x;
const int cy = c.y + y;
const int cz = c.z + z;
if (!grid_.inBounds(cx, cy, cz)) continue;
const int cell = grid_.cellIndex(cx, cy, cz);
for (int j = cellHead_[size_t(cell)]; j >= 0; j = cellNext_[size_t(j)]) {
const glm::vec3 dx = pi - particles_[size_t(j)].pos;
const float r2 = glm::dot(dx, dx);
if (r2 < h2) {
dens += fluidPoly6(r2, h);
gradSum += fluidSpikyGrad(dx, h);
}
}
}
}
}
densities_[size_t(i)] = dens;
gradSums_[size_t(i)] = gradSum;
}
}
void FluidSimulation::computeLambdas() {
const float rho0 = std::max(params_.restDensity, 1e-6f);
for (int i = 0; i < count_; ++i) {
const float rho = densities_[size_t(i)];
const float C = std::max(0.f, rho / rho0 - 1.f);
const float gradSq = glm::dot(gradSums_[size_t(i)], gradSums_[size_t(i)]);
lambdas_[size_t(i)] = -C / (gradSq + 1e-6f);
}
}
void FluidSimulation::applyPositionCorrections() {
const float h = params_.supportRadius;
const float h2 = h * h;
const float rho0 = std::max(params_.restDensity, 1e-6f);
const float radius = params_.particleRadius;
for (int i = 0; i < count_; ++i) {
const glm::vec3 pi = particles_[size_t(i)].pos;
const float li = lambdas_[size_t(i)];
const glm::ivec3 c = grid_.cellOf(pi);
glm::vec3 delta(0.f);
for (int z = -1; z <= 1; ++z) {
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const int cx = c.x + x;
const int cy = c.y + y;
const int cz = c.z + z;
if (!grid_.inBounds(cx, cy, cz)) continue;
const int cell = grid_.cellIndex(cx, cy, cz);
for (int j = cellHead_[size_t(cell)]; j >= 0; j = cellNext_[size_t(j)]) {
if (j == i) continue;
const glm::vec3 dx = pi - particles_[size_t(j)].pos;
const float r2 = glm::dot(dx, dx);
if (r2 < h2) delta += (li + lambdas_[size_t(j)]) * fluidSpikyGrad(dx, h);
}
}
}
}
glm::vec3 p = pi + delta / rho0;
// Clamp the PBF position correction (standard PBF stabilization):
// overlapping particles would otherwise amplify the spiky gradient
// unboundedly and fling the drop apart.
const float maxDelta = 0.0005f * h;
const glm::vec3 correction = delta / rho0;
const float cl = glm::length(correction);
if (cl > maxDelta && cl > 1e-9f) p = pi + correction * (maxDelta / cl);
const float d = sdf_.sample(p);
{
glm::vec3 n = sdf_.gradient(p);
const float nl = glm::length(n);
if (nl > 1e-6f)
n /= nl;
else
n = glm::vec3(0.f, 1.f, 0.f);
p = p - n * (d - radius);
}
particles_[size_t(i)].pos = p;
}
}
void FluidSimulation::integrate(float dt) {
const float h = params_.supportRadius;
const float h2 = h * h;
const float radius = params_.particleRadius;
for (int i = 0; i < count_; ++i) {
glm::vec3 p = particles_[size_t(i)].pos;
glm::vec3 v = particles_[size_t(i)].vel;
// XSPH viscosity + Bingham yield stress + Akinci-style cohesion.
if (params_.viscosity > 0.f || params_.cohesion > 0.f || params_.yieldStress > 0.f) {
glm::vec3 viscAcc(0.f);
glm::vec3 cohesionAcc(0.f);
float shearAcc = 0.f;
float neighborWeight = 0.f;
float cohesionWeight = 0.f;
const float particleVolume = std::pow(2.f * radius, 3.f);
const glm::ivec3 c = grid_.cellOf(p);
for (int z = -1; z <= 1; ++z) {
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const int cx = c.x + x;
const int cy = c.y + y;
const int cz = c.z + z;
if (!grid_.inBounds(cx, cy, cz)) continue;
const int cell = grid_.cellIndex(cx, cy, cz);
for (int j = cellHead_[size_t(cell)]; j >= 0; j = cellNext_[size_t(j)]) {
if (j == i) continue;
const glm::vec3 dx = p - particles_[size_t(j)].pos;
const float r2 = glm::dot(dx, dx);
if (r2 < h2) {
const float weight = fluidPoly6(r2, h) * particleVolume;
if (params_.viscosity > 0.f || params_.yieldStress > 0.f) {
viscAcc += (particles_[size_t(j)].vel - v) * weight;
neighborWeight += weight;
}
if (params_.yieldStress > 0.f)
shearAcc += glm::length(particles_[size_t(j)].vel - v) * weight;
if (params_.cohesion > 0.f) {
cohesionAcc -= dx * weight;
cohesionWeight += weight;
}
}
}
}
}
}
// Bingham plastic: effective viscosity grows as shear rate drops,
// so low-shear mud "freezes" and piles up instead of spreading.
float effectiveViscosity = params_.viscosity;
if (params_.yieldStress > 0.f && shearAcc > 1e-6f) effectiveViscosity += params_.yieldStress / shearAcc;
if (neighborWeight > 1e-6f) v += (viscAcc / neighborWeight) * std::clamp(effectiveViscosity * dt, 0.f, 1.f);
glm::vec3 cohesionNormal = sdf_.gradient(p);
if (glm::length(cohesionNormal) > 1e-6f)
cohesionNormal = glm::normalize(cohesionNormal);
else
cohesionNormal = glm::vec3(0.f, 1.f, 0.f);
cohesionAcc -= cohesionNormal * glm::dot(cohesionAcc, cohesionNormal);
if (cohesionWeight > 1e-6f)
v += fluidClampSpeed((cohesionAcc / cohesionWeight) * (params_.cohesion * 100.f * dt), radius);
}
v += params_.gravity * dt;
v *= std::max(0.f, 1.f - params_.damping * dt);
if (params_.adhesion > 0.f) v *= std::exp(-params_.adhesion * 300.f * dt);
v = fluidClampSpeed(v, params_.maxVelocity);
p += v * dt;
// Project onto the solid surface and kill inward normal velocity so
// gravity only drives tangential flow (water film stays on the model).
float d = sdf_.sample(p);
glm::vec3 n = sdf_.gradient(p);
const float nl = glm::length(n);
if (nl > 1e-6f)
n /= nl;
else
n = glm::vec3(0.f, 1.f, 0.f);
{
p = p - n * (d - radius);
const float vn = glm::dot(v, n);
v -= n * vn;
d = radius;
}
// Adhesion: pull the film toward the solid while within range.
if (params_.adhesion > 0.f && d < h) v += -n * (params_.adhesion * std::max(0.f, 1.f - d / h) * dt * 30.f);
particles_[size_t(i)].pos = p;
particles_[size_t(i)].vel = v;
}
}
} // namespace eve::fluids