292 lines
12 KiB
C++
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
|