1221 lines
44 KiB
C++
1221 lines
44 KiB
C++
#include "physics/Cloth3D.h"
|
|
|
|
#include "common/Exception.h"
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Canvas.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "physics/Body3D.h"
|
|
#include "physics/World3D.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <exception>
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
namespace eve::physics {
|
|
|
|
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
|
|
using eve::graphics::Color;
|
|
using eve::graphics::Mesh;
|
|
|
|
namespace {
|
|
constexpr float kPi = 3.14159265358979323846f;
|
|
|
|
int64_t pairKey(int lo, int hi) {
|
|
return (int64_t(lo) << 32) | int64_t(hi);
|
|
}
|
|
|
|
// Minimal 3D vector helpers for the triangle-level self-collision pass.
|
|
struct V3 {
|
|
float x = 0.f, y = 0.f, z = 0.f;
|
|
};
|
|
|
|
V3 sub(const V3 &a, const V3 &b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
|
V3 add(const V3 &a, const V3 &b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
|
V3 mul(float s, const V3 &a) { return {s * a.x, s * a.y, s * a.z}; }
|
|
V3 fma(const V3 &a, float s, const V3 &b) { return {a.x * s + b.x, a.y * s + b.y, a.z * s + b.z}; }
|
|
float dot(const V3 &a, const V3 &b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
|
V3 cross(const V3 &a, const V3 &b) {
|
|
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
|
}
|
|
float vlen(const V3 &a) { return std::sqrt(dot(a, a)); }
|
|
V3 vnorm(const V3 &a) {
|
|
const float l = vlen(a);
|
|
return l > 1e-8f ? mul(1.f / l, a) : V3{0.f, 1.f, 0.f};
|
|
}
|
|
|
|
// Rotate vector v around the given unit axis by angle (right-hand rule).
|
|
V3 rotateAround(const V3 &v, const V3 &axis, float angle) {
|
|
const float c = std::cos(angle);
|
|
const float sn = std::sin(angle);
|
|
const V3 cv = cross(axis, v);
|
|
const float d = dot(axis, v);
|
|
return add(add(mul(c, v), mul(sn, cv)), mul(d * (1.f - c), axis));
|
|
}
|
|
|
|
// Signed angle from a to b around the unit axis u (positive = CCW along +u).
|
|
float signedAngle(const V3 &a, const V3 &b, const V3 &u) {
|
|
return std::atan2(dot(cross(a, b), u), dot(a, b));
|
|
}
|
|
|
|
// Closest point on segment [a,b] to point p (Ericson, RTCD ch. 5).
|
|
V3 closestPointSegment(const V3 &a, const V3 &b, const V3 &p) {
|
|
const V3 ab = sub(b, a);
|
|
const float len2 = dot(ab, ab);
|
|
if (len2 < 1e-12f) return a;
|
|
const float t = std::clamp(dot(sub(p, a), ab) / len2, 0.f, 1.f);
|
|
return fma(ab, t, a);
|
|
}
|
|
|
|
// Closest point on triangle (a,b,c) to point p (Ericson, RTCD ch. 5).
|
|
V3 closestPointTriangle(const V3 &a, const V3 &b, const V3 &c, const V3 &p) {
|
|
const V3 ab = sub(b, a);
|
|
const V3 ac = sub(c, a);
|
|
const V3 ap = sub(p, a);
|
|
const float d1 = dot(ab, ap);
|
|
const float d2 = dot(ac, ap);
|
|
if (d1 <= 0.f && d2 <= 0.f) return a;
|
|
|
|
const V3 bp = sub(p, b);
|
|
const float d3 = dot(ab, bp);
|
|
const float d4 = dot(ac, bp);
|
|
if (d3 >= 0.f && d4 <= d3) return b;
|
|
|
|
const float vc = d1 * d4 - d3 * d2;
|
|
if (vc <= 0.f && d1 >= 0.f && d3 <= 0.f) {
|
|
const float t = d1 / (d1 - d3);
|
|
return fma(ab, t, a);
|
|
}
|
|
|
|
const V3 cp = sub(p, c);
|
|
const float d5 = dot(ab, cp);
|
|
const float d6 = dot(ac, cp);
|
|
if (d6 >= 0.f && d5 <= d6) return c;
|
|
|
|
const float vb = d5 * d2 - d1 * d6;
|
|
if (vb <= 0.f && d2 >= 0.f && d6 <= 0.f) {
|
|
const float t = d2 / (d2 - d6);
|
|
return fma(ac, t, a);
|
|
}
|
|
|
|
const float va = d3 * d6 - d5 * d4;
|
|
if (va <= 0.f && (d4 - d3) >= 0.f && (d5 - d6) >= 0.f) {
|
|
const float t = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
|
return fma(sub(c, b), t, b);
|
|
}
|
|
|
|
const float denom = 1.f / (va + vb + vc);
|
|
const float v = vb * denom;
|
|
const float w = vc * denom;
|
|
return add(fma(ab, v, a), mul(w, ac));
|
|
}
|
|
|
|
// Closest points between segments (p1,p2) and (q1,q2); returns distance.
|
|
float closestPointSegments(const V3 &p1, const V3 &p2, const V3 &q1, const V3 &q2, V3 &cp1,
|
|
V3 &cp2) {
|
|
const V3 d1 = sub(p2, p1);
|
|
const V3 d2 = sub(q2, q1);
|
|
const V3 r = sub(p1, q1);
|
|
const float a = dot(d1, d1);
|
|
const float e = dot(d2, d2);
|
|
const float f = dot(d2, r);
|
|
float s = 0.f;
|
|
float t = 0.f;
|
|
if (a <= 1e-12f && e <= 1e-12f) {
|
|
s = t = 0.f;
|
|
} else if (a <= 1e-12f) {
|
|
s = 0.f;
|
|
t = std::clamp(f / e, 0.f, 1.f);
|
|
} else {
|
|
const float c = dot(d1, r);
|
|
if (e <= 1e-12f) {
|
|
t = 0.f;
|
|
s = std::clamp(-c / a, 0.f, 1.f);
|
|
} else {
|
|
const float b = dot(d1, d2);
|
|
const float denom = a * e - b * b;
|
|
s = denom > 1e-12f ? std::clamp((b * f - c * e) / denom, 0.f, 1.f) : 0.f;
|
|
t = (b * s + f) / e;
|
|
if (t < 0.f) {
|
|
t = 0.f;
|
|
s = std::clamp(-c / a, 0.f, 1.f);
|
|
} else if (t > 1.f) {
|
|
t = 1.f;
|
|
s = std::clamp((b - c) / a, 0.f, 1.f);
|
|
}
|
|
}
|
|
}
|
|
cp1 = fma(d1, s, p1);
|
|
cp2 = fma(d2, t, q1);
|
|
return vlen(sub(cp1, cp2));
|
|
}
|
|
|
|
bool pointInTriangle(const V3 &p, const V3 &a, const V3 &b, const V3 &c) {
|
|
const V3 n = cross(sub(b, a), sub(c, a));
|
|
const V3 pa = cross(sub(b, a), sub(p, a));
|
|
const V3 pb = cross(sub(c, b), sub(p, b));
|
|
const V3 pc = cross(sub(a, c), sub(p, c));
|
|
return dot(pa, n) >= -1e-8f && dot(pb, n) >= -1e-8f && dot(pc, n) >= -1e-8f;
|
|
}
|
|
|
|
// Closest distance between two triangles plus the indices of the vertices
|
|
// forming the closest feature on each side (1, 2 or 3 vertices).
|
|
float triangleDistance(const V3 *A, const V3 *B, int featA[3], int featB[3], int &countA,
|
|
int &countB, V3 &cpA, V3 &cpB) {
|
|
float best = std::numeric_limits<float>::max();
|
|
countA = countB = 0;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const V3 cp = closestPointTriangle(B[0], B[1], B[2], A[i]);
|
|
const float d = vlen(sub(A[i], cp));
|
|
if (d < best) {
|
|
best = d;
|
|
cpA = A[i];
|
|
cpB = cp;
|
|
featA[0] = i;
|
|
featB[0] = 0;
|
|
featB[1] = 1;
|
|
featB[2] = 2;
|
|
countA = 1;
|
|
countB = 3;
|
|
}
|
|
}
|
|
for (int j = 0; j < 3; ++j) {
|
|
const V3 cp = closestPointTriangle(A[0], A[1], A[2], B[j]);
|
|
const float d = vlen(sub(B[j], cp));
|
|
if (d < best) {
|
|
best = d;
|
|
cpA = cp;
|
|
cpB = B[j];
|
|
featA[0] = 0;
|
|
featA[1] = 1;
|
|
featA[2] = 2;
|
|
featB[0] = j;
|
|
countA = 3;
|
|
countB = 1;
|
|
}
|
|
}
|
|
const int aEdges[3][2] = {{0, 1}, {1, 2}, {2, 0}};
|
|
const int bEdges[3][2] = {{0, 1}, {1, 2}, {2, 0}};
|
|
for (const auto &ea : aEdges) {
|
|
for (const auto &eb : bEdges) {
|
|
V3 cp1;
|
|
V3 cp2;
|
|
const float d = closestPointSegments(A[ea[0]], A[ea[1]], B[eb[0]], B[eb[1]], cp1,
|
|
cp2);
|
|
if (d < best) {
|
|
best = d;
|
|
cpA = cp1;
|
|
cpB = cp2;
|
|
featA[0] = ea[0];
|
|
featA[1] = ea[1];
|
|
featB[0] = eb[0];
|
|
featB[1] = eb[1];
|
|
countA = 2;
|
|
countB = 2;
|
|
}
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
} // namespace
|
|
|
|
Cloth3D::Cloth3D(int cols, int rows, float spacing, float originX, float originY, float originZ)
|
|
: cols_(cols), rows_(rows), spacing_(spacing), originX_(originX), originY_(originY),
|
|
originZ_(originZ) {
|
|
if (cols_ < 2 || rows_ < 2)
|
|
throw Exception("Cloth3D: cols and rows must be >= 2");
|
|
if (spacing_ <= 0.f)
|
|
throw Exception("Cloth3D: spacing must be > 0");
|
|
|
|
particles_.resize(static_cast<size_t>(cols_ * rows_));
|
|
for (int r = 0; r < rows_; ++r) {
|
|
for (int c = 0; c < cols_; ++c) {
|
|
Particle &p = particles_[static_cast<size_t>(r * cols_ + c)];
|
|
p.x = p.px = originX_ + float(c) * spacing_;
|
|
p.y = p.py = originY_;
|
|
p.z = p.pz = originZ_ + float(r) * spacing_;
|
|
p.pinned = false;
|
|
}
|
|
}
|
|
rebuildLinks();
|
|
rebuildTriangles();
|
|
pinTopRow();
|
|
}
|
|
|
|
Cloth3D::~Cloth3D() { destroy(); }
|
|
|
|
void Cloth3D::destroy() { destroyed_ = true; }
|
|
|
|
void Cloth3D::reset() {
|
|
if (cols_ < 2 || rows_ < 2) return;
|
|
particles_.clear();
|
|
particles_.resize(static_cast<size_t>(cols_ * rows_));
|
|
for (int r = 0; r < rows_; ++r) {
|
|
for (int c = 0; c < cols_; ++c) {
|
|
Particle &p = particles_[static_cast<size_t>(r * cols_ + c)];
|
|
p.x = p.px = originX_ + float(c) * spacing_;
|
|
p.y = p.py = originY_;
|
|
p.z = p.pz = originZ_ + float(r) * spacing_;
|
|
p.pinned = false;
|
|
}
|
|
}
|
|
rebuildLinks();
|
|
rebuildTriangles();
|
|
pinTopRow();
|
|
grabIndex_ = -1;
|
|
forceX_ = forceY_ = forceZ_ = 0.f;
|
|
interactStrength_ = 0.f;
|
|
}
|
|
|
|
void Cloth3D::rebuildLinks() {
|
|
links_.clear();
|
|
auto add = [&](int a, int b) {
|
|
if (a < 0 || b < 0 || a >= getParticleCount() || b >= getParticleCount()) return;
|
|
Link link;
|
|
link.a = a;
|
|
link.b = b;
|
|
const Particle &pa = particles_[static_cast<size_t>(a)];
|
|
const Particle &pb = particles_[static_cast<size_t>(b)];
|
|
const float dx = pb.x - pa.x;
|
|
const float dy = pb.y - pa.y;
|
|
const float dz = pb.z - pa.z;
|
|
link.rest = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (link.rest > 1e-4f) links_.push_back(link);
|
|
};
|
|
|
|
for (int r = 0; r < rows_; ++r) {
|
|
for (int c = 0; c < cols_; ++c) {
|
|
const int i = r * cols_ + c;
|
|
// Structural
|
|
if (c + 1 < cols_) add(i, i + 1);
|
|
if (r + 1 < rows_) add(i, i + cols_);
|
|
// Shear
|
|
if (c + 1 < cols_ && r + 1 < rows_) add(i, i + cols_ + 1);
|
|
if (c > 0 && r + 1 < rows_) add(i, i + cols_ - 1);
|
|
// Bend (every other)
|
|
if (c + 2 < cols_) add(i, i + 2);
|
|
if (r + 2 < rows_) add(i, i + cols_ * 2);
|
|
}
|
|
}
|
|
buildLinkKeys();
|
|
}
|
|
|
|
void Cloth3D::buildLinkKeys() {
|
|
linkKeys_.clear();
|
|
for (const Link &link : links_) {
|
|
const int lo = std::min(link.a, link.b);
|
|
const int hi = std::max(link.a, link.b);
|
|
linkKeys_.insert(pairKey(lo, hi));
|
|
}
|
|
}
|
|
|
|
bool Cloth3D::areLinked(int a, int b) const {
|
|
if (a == b) return true;
|
|
const int lo = std::min(a, b);
|
|
const int hi = std::max(a, b);
|
|
return linkKeys_.find(pairKey(lo, hi)) != linkKeys_.end();
|
|
}
|
|
|
|
void Cloth3D::rebuildTriangles() {
|
|
triangles_.clear();
|
|
foldPairs_.clear();
|
|
|
|
// Two triangles per quad, wound so the +Y side is the front face.
|
|
std::unordered_map<int64_t, std::vector<int>> edgeTris;
|
|
auto addTri = [&](int a, int b, int c, std::unordered_map<int64_t, std::vector<int>> &map) {
|
|
Tri tri;
|
|
tri.v[0] = a;
|
|
tri.v[1] = b;
|
|
tri.v[2] = c;
|
|
const int idx = static_cast<int>(triangles_.size());
|
|
const int edges[3][2] = {{a, b}, {b, c}, {c, a}};
|
|
for (const auto &e : edges) {
|
|
map[pairKey(std::min(e[0], e[1]), std::max(e[0], e[1]))].push_back(idx);
|
|
}
|
|
triangles_.push_back(tri);
|
|
};
|
|
|
|
for (int r = 0; r + 1 < rows_; ++r) {
|
|
for (int c = 0; c + 1 < cols_; ++c) {
|
|
const int a = r * cols_ + c;
|
|
const int b = r * cols_ + c + 1;
|
|
const int cc = (r + 1) * cols_ + c + 1;
|
|
const int d = (r + 1) * cols_ + c;
|
|
addTri(a, d, b, edgeTris);
|
|
addTri(d, cc, b, edgeTris);
|
|
}
|
|
}
|
|
|
|
for (const auto &entry : edgeTris) {
|
|
if (entry.second.size() != 2) continue;
|
|
const Tri &t0 = triangles_[static_cast<size_t>(entry.second[0])];
|
|
const Tri &t1 = triangles_[static_cast<size_t>(entry.second[1])];
|
|
const int e0 = int(entry.first >> 32);
|
|
const int e1 = static_cast<int>(static_cast<uint32_t>(entry.first));
|
|
// Shared edge endpoints e0/e1; opposite vertices are the third of each tri.
|
|
int k = -1;
|
|
for (int v : t0.v) {
|
|
if (v != e0 && v != e1) { k = v; break; }
|
|
}
|
|
int l = -1;
|
|
for (int v : t1.v) {
|
|
if (v != e0 && v != e1) { l = v; break; }
|
|
}
|
|
if (k < 0 || l < 0) continue;
|
|
FoldPair pair;
|
|
pair.a = e0;
|
|
pair.b = e1;
|
|
pair.k = k;
|
|
pair.l = l;
|
|
foldPairs_.push_back(pair);
|
|
}
|
|
}
|
|
|
|
bool Cloth3D::validIndex(int index) const {
|
|
return index >= 0 && index < getParticleCount();
|
|
}
|
|
|
|
void Cloth3D::setGravity(float gx, float gy, float gz) {
|
|
gravityX_ = gx;
|
|
gravityY_ = gy;
|
|
gravityZ_ = gz;
|
|
}
|
|
|
|
void Cloth3D::setStiffness(float stiffness) {
|
|
stiffness_ = std::clamp(stiffness, 0.f, 1.f);
|
|
}
|
|
|
|
void Cloth3D::setIterations(int iterations) {
|
|
iterations_ = std::max(1, iterations);
|
|
}
|
|
|
|
void Cloth3D::setDamping(float damping) {
|
|
damping_ = std::clamp(damping, 0.f, 1.f);
|
|
}
|
|
|
|
void Cloth3D::setParticleSize(float size) {
|
|
particleSize_ = std::max(0.01f, size);
|
|
}
|
|
|
|
void Cloth3D::setParticleMass(float mass) {
|
|
particleMass_ = std::max(1e-4f, mass);
|
|
}
|
|
|
|
void Cloth3D::setSelfCollision(bool on) { selfCollision_ = on; }
|
|
|
|
void Cloth3D::setFoldStiffness(float k) {
|
|
foldStiffness_ = std::clamp(k, 0.f, 1.f);
|
|
}
|
|
|
|
void Cloth3D::setMaxFoldAngle(float degrees) {
|
|
maxFoldAngle_ = std::clamp(degrees, 0.f, 180.f) * kPi / 180.f;
|
|
}
|
|
|
|
void Cloth3D::setBounds(float x, float y, float z, float w, float h, float d) {
|
|
if (w <= 0.f || h <= 0.f || d <= 0.f) {
|
|
clearBounds();
|
|
return;
|
|
}
|
|
hasBounds_ = true;
|
|
boundX_ = x;
|
|
boundY_ = y;
|
|
boundZ_ = z;
|
|
boundW_ = w;
|
|
boundH_ = h;
|
|
boundD_ = d;
|
|
}
|
|
|
|
void Cloth3D::clearBounds() { hasBounds_ = false; }
|
|
|
|
void Cloth3D::pin(int index) {
|
|
if (!validIndex(index)) throw Exception("Cloth3D.pin: index out of range");
|
|
particles_[static_cast<size_t>(index)].pinned = true;
|
|
}
|
|
|
|
void Cloth3D::unpin(int index) {
|
|
if (!validIndex(index)) throw Exception("Cloth3D.unpin: index out of range");
|
|
particles_[static_cast<size_t>(index)].pinned = false;
|
|
}
|
|
|
|
void Cloth3D::pinTopRow() {
|
|
for (int c = 0; c < cols_; ++c)
|
|
particles_[static_cast<size_t>(c)].pinned = true;
|
|
}
|
|
|
|
bool Cloth3D::isPinned(int index) const {
|
|
if (!validIndex(index)) return false;
|
|
return particles_[static_cast<size_t>(index)].pinned;
|
|
}
|
|
|
|
int Cloth3D::grabAt(float x, float y, float z, float radius) {
|
|
grabIndex_ = -1;
|
|
float best = radius * radius;
|
|
for (int i = 0; i < getParticleCount(); ++i) {
|
|
const Particle &p = particles_[static_cast<size_t>(i)];
|
|
if (p.pinned) continue;
|
|
const float dx = p.x - x;
|
|
const float dy = p.y - y;
|
|
const float dz = p.z - z;
|
|
const float d2 = dx * dx + dy * dy + dz * dz;
|
|
if (d2 <= best) {
|
|
best = d2;
|
|
grabIndex_ = i;
|
|
}
|
|
}
|
|
if (grabIndex_ >= 0) moveGrab(x, y, z);
|
|
return grabIndex_;
|
|
}
|
|
|
|
void Cloth3D::moveGrab(float x, float y, float z) {
|
|
grabX_ = x;
|
|
grabY_ = y;
|
|
grabZ_ = z;
|
|
if (!validIndex(grabIndex_)) return;
|
|
Particle &p = particles_[static_cast<size_t>(grabIndex_)];
|
|
p.x = x;
|
|
p.y = y;
|
|
p.z = z;
|
|
p.px = x;
|
|
p.py = y;
|
|
p.pz = z;
|
|
}
|
|
|
|
void Cloth3D::releaseGrab() { grabIndex_ = -1; }
|
|
|
|
void Cloth3D::applyForce(float fx, float fy, float fz) {
|
|
forceX_ += fx;
|
|
forceY_ += fy;
|
|
forceZ_ += fz;
|
|
}
|
|
|
|
void Cloth3D::interactAt(float x, float y, float z, float radius, float strength) {
|
|
interactX_ = x;
|
|
interactY_ = y;
|
|
interactZ_ = z;
|
|
interactRadius_ = std::max(0.f, radius);
|
|
interactStrength_ = strength;
|
|
}
|
|
|
|
void Cloth3D::setCollideWorld(World3D *world) { world_ = world; }
|
|
|
|
void Cloth3D::setColor(float r, float g, float b, float a) {
|
|
colorR_ = r;
|
|
colorG_ = g;
|
|
colorB_ = b;
|
|
colorA_ = a;
|
|
}
|
|
|
|
float Cloth3D::getParticleX(int index) const {
|
|
if (!validIndex(index)) return 0.f;
|
|
return particles_[static_cast<size_t>(index)].x;
|
|
}
|
|
|
|
float Cloth3D::getParticleY(int index) const {
|
|
if (!validIndex(index)) return 0.f;
|
|
return particles_[static_cast<size_t>(index)].y;
|
|
}
|
|
|
|
float Cloth3D::getParticleZ(int index) const {
|
|
if (!validIndex(index)) return 0.f;
|
|
return particles_[static_cast<size_t>(index)].z;
|
|
}
|
|
|
|
void Cloth3D::setParticlePosition(int index, float x, float y, float z) {
|
|
if (!validIndex(index)) throw Exception("Cloth3D.setParticlePosition: index out of range");
|
|
Particle &p = particles_[static_cast<size_t>(index)];
|
|
p.x = p.px = x;
|
|
p.y = p.py = y;
|
|
p.z = p.pz = z;
|
|
}
|
|
|
|
int64_t Cloth3D::cellKey(int cx, int cy, int cz) const {
|
|
// 21 bits per axis keeps the packed key inside a signed 64-bit int.
|
|
return int64_t(uint32_t(cx) & 0x1fffffu) |
|
|
(int64_t(uint32_t(cy) & 0x1fffffu) << 21) |
|
|
(int64_t(uint32_t(cz) & 0x1fffffu) << 42);
|
|
}
|
|
|
|
void Cloth3D::rebuildHash() {
|
|
hash_.clear();
|
|
const float cell = std::max(1e-3f, particleSize_ * 2.f);
|
|
const float inv = 1.f / cell;
|
|
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));
|
|
const int cz = int(std::floor(p.z * inv));
|
|
hash_[cellKey(cx, cy, cz)].push_back(i);
|
|
}
|
|
}
|
|
|
|
void Cloth3D::integrate(float dt) {
|
|
if (dt <= 0.f) return;
|
|
const float ax = gravityX_ + forceX_;
|
|
const float ay = gravityY_ + forceY_;
|
|
const float az = gravityZ_ + forceZ_;
|
|
const float damp = 1.f - damping_;
|
|
const bool hasInteract = interactRadius_ > 0.f && interactStrength_ != 0.f;
|
|
|
|
for (Particle &p : particles_) {
|
|
if (p.pinned) {
|
|
p.px = p.x;
|
|
p.py = p.y;
|
|
p.pz = p.z;
|
|
continue;
|
|
}
|
|
const float vx = (p.x - p.px) * damp;
|
|
const float vy = (p.y - p.py) * damp;
|
|
const float vz = (p.z - p.pz) * damp;
|
|
p.px = p.x;
|
|
p.py = p.y;
|
|
p.pz = p.z;
|
|
p.x += vx + ax * dt * dt;
|
|
p.y += vy + ay * dt * dt;
|
|
p.z += vz + az * dt * dt;
|
|
if (hasInteract) {
|
|
const float dx = interactX_ - p.x;
|
|
const float dy = interactY_ - p.y;
|
|
const float dz = interactZ_ - p.z;
|
|
const float r2 = dx * dx + dy * dy + dz * dz;
|
|
const float R2 = interactRadius_ * interactRadius_;
|
|
if (r2 < R2 && r2 > 1e-6f) {
|
|
const float r = std::sqrt(r2);
|
|
const float w = 1.f - r / interactRadius_;
|
|
const float a = interactStrength_ * w * dt * dt;
|
|
p.x += (dx / r) * a;
|
|
p.y += (dy / r) * a;
|
|
p.z += (dz / r) * a;
|
|
}
|
|
}
|
|
constexpr float maxSpeed = 9.f; // m/s
|
|
const float maxDisp = maxSpeed * dt;
|
|
const float dvx = p.x - p.px;
|
|
const float dvy = p.y - p.py;
|
|
const float dvz = p.z - p.pz;
|
|
const float v2 = dvx * dvx + dvy * dvy + dvz * dvz;
|
|
if (v2 > maxDisp * maxDisp) {
|
|
const float s = maxDisp / std::sqrt(v2);
|
|
p.px = p.x - dvx * s;
|
|
p.py = p.y - dvy * s;
|
|
p.pz = p.z - dvz * s;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::solveConstraints() {
|
|
for (int iter = 0; iter < iterations_; ++iter) {
|
|
for (const Link &link : links_) {
|
|
Particle &a = particles_[static_cast<size_t>(link.a)];
|
|
Particle &b = particles_[static_cast<size_t>(link.b)];
|
|
if (a.pinned && b.pinned) continue;
|
|
float dx = b.x - a.x;
|
|
float dy = b.y - a.y;
|
|
float dz = b.z - a.z;
|
|
const float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (dist < 1e-5f) continue;
|
|
const float diff = (dist - link.rest) / dist * stiffness_;
|
|
if (a.pinned) {
|
|
b.x -= dx * diff;
|
|
b.y -= dy * diff;
|
|
b.z -= dz * diff;
|
|
} else if (b.pinned) {
|
|
a.x += dx * diff;
|
|
a.y += dy * diff;
|
|
a.z += dz * diff;
|
|
} else {
|
|
const float half = diff * 0.5f;
|
|
a.x += dx * half;
|
|
a.y += dy * half;
|
|
a.z += dz * half;
|
|
b.x -= dx * half;
|
|
b.y -= dy * half;
|
|
b.z -= dz * half;
|
|
}
|
|
}
|
|
if (validIndex(grabIndex_)) {
|
|
Particle &g = particles_[static_cast<size_t>(grabIndex_)];
|
|
g.x = grabX_;
|
|
g.y = grabY_;
|
|
g.z = grabZ_;
|
|
g.px = grabX_;
|
|
g.py = grabY_;
|
|
g.pz = grabZ_;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::solveFoldConstraint() {
|
|
if (foldStiffness_ <= 0.f || maxFoldAngle_ >= kPi || foldPairs_.empty()) return;
|
|
// Fold pairs share edge (a,b) with opposite vertices k (tri 1) and l (tri 2).
|
|
// For a flat sheet the in-plane directions m1,m2 point to opposite sides
|
|
// (theta = pi); folding brings them toward the same side (theta -> 0). When
|
|
// theta drops below pi - maxFoldAngle, rotate both triangles rigidly around
|
|
// the shared edge to reopen the fold to the limit.
|
|
const float thetaMin = kPi - maxFoldAngle_;
|
|
const float s = foldStiffness_;
|
|
for (const FoldPair &pair : foldPairs_) {
|
|
Particle &pa = particles_[static_cast<size_t>(pair.a)];
|
|
Particle &pb = particles_[static_cast<size_t>(pair.b)];
|
|
Particle &pk = particles_[static_cast<size_t>(pair.k)];
|
|
Particle &pl = particles_[static_cast<size_t>(pair.l)];
|
|
if (pa.pinned && pb.pinned && pk.pinned && pl.pinned) continue;
|
|
|
|
const V3 paV{pa.x, pa.y, pa.z};
|
|
const V3 pbV{pb.x, pb.y, pb.z};
|
|
V3 pkV{pk.x, pk.y, pk.z};
|
|
V3 plV{pl.x, pl.y, pl.z};
|
|
|
|
const V3 u = vnorm(sub(pbV, paV));
|
|
const V3 vk = sub(pkV, paV);
|
|
const V3 vl = sub(plV, paV);
|
|
const V3 h1 = sub(vk, mul(dot(vk, u), u));
|
|
const V3 h2 = sub(vl, mul(dot(vl, u), u));
|
|
const float b1 = vlen(h1);
|
|
const float b2 = vlen(h2);
|
|
if (b1 < 1e-6f || b2 < 1e-6f) continue;
|
|
const V3 m1 = mul(1.f / b1, h1);
|
|
const V3 m2 = mul(1.f / b2, h2);
|
|
const float theta = std::acos(std::clamp(dot(m1, m2), -1.f, 1.f));
|
|
if (theta >= thetaMin) continue;
|
|
|
|
// Reopen the fold along its own path: a pure fold moves m1/m2 from
|
|
// opposite (flat, theta = pi) toward the same side (theta -> 0) with a
|
|
// fixed rotation sign; rotate both triangles back by delta/2 each.
|
|
const float phi = signedAngle(m1, m2, u);
|
|
const float sign = phi >= 0.f ? 1.f : -1.f;
|
|
const float delta = thetaMin - theta;
|
|
const float a1 = -sign * delta * 0.5f * s;
|
|
const float a2 = sign * delta * 0.5f * s;
|
|
if (!pk.pinned) {
|
|
pkV = add(paV, rotateAround(vk, u, a1 * s));
|
|
pk.x = pkV.x;
|
|
pk.y = pkV.y;
|
|
pk.z = pkV.z;
|
|
}
|
|
if (!pl.pinned) {
|
|
plV = add(paV, rotateAround(vl, u, a2 * s));
|
|
pl.x = plV.x;
|
|
pl.y = plV.y;
|
|
pl.z = plV.z;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::solveSelfCollision() {
|
|
if (!selfCollision_ || getParticleCount() == 0) return;
|
|
const float minDist = particleSize_ * 2.f;
|
|
if (minDist <= 0.f) return;
|
|
rebuildHash();
|
|
const float cell = std::max(1e-3f, minDist);
|
|
const float inv = 1.f / cell;
|
|
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));
|
|
const int cz = int(std::floor(pi.z * inv));
|
|
for (int oz = -1; oz <= 1; ++oz) {
|
|
for (int oy = -1; oy <= 1; ++oy) {
|
|
for (int ox = -1; ox <= 1; ++ox) {
|
|
auto it = hash_.find(cellKey(cx + ox, cy + oy, cz + oz));
|
|
if (it == hash_.end()) continue;
|
|
for (int j : it->second) {
|
|
if (j <= i) continue;
|
|
if (areLinked(i, j)) continue;
|
|
Particle &pj = particles_[static_cast<size_t>(j)];
|
|
float dx = pj.x - pi.x;
|
|
float dy = pj.y - pi.y;
|
|
float dz = pj.z - pi.z;
|
|
const float d2 = dx * dx + dy * dy + dz * dz;
|
|
if (d2 >= minDist * minDist || d2 < 1e-8f) continue;
|
|
if (pi.pinned && pj.pinned) continue;
|
|
const float d = std::sqrt(d2);
|
|
const float corr = std::min(0.5f, (minDist - d) / d);
|
|
float wa = 0.5f;
|
|
float wb = 0.5f;
|
|
if (pi.pinned && !pj.pinned) { wa = 0.f; wb = 1.f; }
|
|
else if (pj.pinned && !pi.pinned) { wa = 1.f; wb = 0.f; }
|
|
pi.x -= dx * corr * wa;
|
|
pi.y -= dy * corr * wa;
|
|
pi.z -= dz * corr * wa;
|
|
pj.x += dx * corr * wb;
|
|
pj.y += dy * corr * wb;
|
|
pj.z += dz * corr * wb;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::solveSelfCollisionTriangles() {
|
|
if (!selfCollision_ || triangles_.size() < 2) return;
|
|
const float thickness = std::max(1e-3f, particleSize_ * 2.f);
|
|
const int triCount = static_cast<int>(triangles_.size());
|
|
|
|
std::vector<V3> pos(particles_.size());
|
|
for (size_t i = 0; i < particles_.size(); ++i) {
|
|
pos[i] = {particles_[i].x, particles_[i].y, particles_[i].z};
|
|
}
|
|
|
|
const float cell = thickness;
|
|
const float inv = 1.f / cell;
|
|
std::unordered_map<int64_t, std::vector<int>> triHash;
|
|
auto triCellKey = [](int cx, int cy, int cz) -> int64_t {
|
|
return int64_t(uint32_t(cx) & 0x1fffffu) |
|
|
(int64_t(uint32_t(cy) & 0x1fffffu) << 21) |
|
|
(int64_t(uint32_t(cz) & 0x1fffffu) << 42);
|
|
};
|
|
const auto insertTri = [&](int ti) {
|
|
const Tri &tri = triangles_[static_cast<size_t>(ti)];
|
|
V3 mn = pos[static_cast<size_t>(tri.v[0])];
|
|
V3 mx = mn;
|
|
for (int k = 1; k < 3; ++k) {
|
|
const V3 &p = pos[static_cast<size_t>(tri.v[k])];
|
|
mn.x = std::min(mn.x, p.x);
|
|
mn.y = std::min(mn.y, p.y);
|
|
mn.z = std::min(mn.z, p.z);
|
|
mx.x = std::max(mx.x, p.x);
|
|
mx.y = std::max(mx.y, p.y);
|
|
mx.z = std::max(mx.z, p.z);
|
|
}
|
|
const V3 pad{thickness, thickness, thickness};
|
|
mn = sub(mn, pad);
|
|
mx = add(mx, pad);
|
|
const int x0 = int(std::floor(mn.x * inv));
|
|
const int y0 = int(std::floor(mn.y * inv));
|
|
const int z0 = int(std::floor(mn.z * inv));
|
|
const int x1 = int(std::floor(mx.x * inv));
|
|
const int y1 = int(std::floor(mx.y * inv));
|
|
const int z1 = int(std::floor(mx.z * inv));
|
|
for (int cz = z0; cz <= z1; ++cz)
|
|
for (int cy = y0; cy <= y1; ++cy)
|
|
for (int cx = x0; cx <= x1; ++cx)
|
|
triHash[triCellKey(cx, cy, cz)].push_back(ti);
|
|
};
|
|
for (int ti = 0; ti < triCount; ++ti) insertTri(ti);
|
|
|
|
const auto sharesVertex = [&](const Tri &a, const Tri &b) {
|
|
for (int i = 0; i < 3; ++i)
|
|
for (int j = 0; j < 3; ++j)
|
|
if (a.v[i] == b.v[j]) return true;
|
|
return false;
|
|
};
|
|
|
|
std::unordered_set<int64_t> processed;
|
|
std::vector<int> cells;
|
|
int featA[3];
|
|
int featB[3];
|
|
|
|
for (int i = 0; i < triCount; ++i) {
|
|
const Tri &triI = triangles_[static_cast<size_t>(i)];
|
|
const V3 &pa0 = pos[static_cast<size_t>(triI.v[0])];
|
|
V3 mn = pa0;
|
|
V3 mx = mn;
|
|
for (int k = 1; k < 3; ++k) {
|
|
const V3 &p = pos[static_cast<size_t>(triI.v[k])];
|
|
mn.x = std::min(mn.x, p.x);
|
|
mn.y = std::min(mn.y, p.y);
|
|
mn.z = std::min(mn.z, p.z);
|
|
mx.x = std::max(mx.x, p.x);
|
|
mx.y = std::max(mx.y, p.y);
|
|
mx.z = std::max(mx.z, p.z);
|
|
}
|
|
const V3 pad{thickness, thickness, thickness};
|
|
mn = sub(mn, pad);
|
|
mx = add(mx, pad);
|
|
const int x0 = int(std::floor(mn.x * inv));
|
|
const int y0 = int(std::floor(mn.y * inv));
|
|
const int z0 = int(std::floor(mn.z * inv));
|
|
const int x1 = int(std::floor(mx.x * inv));
|
|
const int y1 = int(std::floor(mx.y * inv));
|
|
const int z1 = int(std::floor(mx.z * inv));
|
|
cells.clear();
|
|
for (int cz = z0; cz <= z1; ++cz)
|
|
for (int cy = y0; cy <= y1; ++cy)
|
|
for (int cx = x0; cx <= x1; ++cx) {
|
|
auto it = triHash.find(triCellKey(cx, cy, cz));
|
|
if (it != triHash.end()) cells.insert(cells.end(), it->second.begin(),
|
|
it->second.end());
|
|
}
|
|
for (int j : cells) {
|
|
if (j <= i) continue;
|
|
const int64_t key = pairKey(i, j);
|
|
if (!processed.insert(key).second) continue;
|
|
const Tri &triJ = triangles_[static_cast<size_t>(j)];
|
|
if (sharesVertex(triI, triJ)) continue;
|
|
|
|
const V3 A[3] = {pos[static_cast<size_t>(triI.v[0])],
|
|
pos[static_cast<size_t>(triI.v[1])],
|
|
pos[static_cast<size_t>(triI.v[2])]};
|
|
const V3 B[3] = {pos[static_cast<size_t>(triJ.v[0])],
|
|
pos[static_cast<size_t>(triJ.v[1])],
|
|
pos[static_cast<size_t>(triJ.v[2])]};
|
|
V3 cpA;
|
|
V3 cpB;
|
|
int countA = 0;
|
|
int countB = 0;
|
|
const float d = triangleDistance(A, B, featA, featB, countA, countB, cpA, cpB);
|
|
if (d >= thickness) continue;
|
|
|
|
V3 n = vnorm(sub(cpB, cpA));
|
|
if (vlen(sub(cpB, cpA)) < 1e-6f) {
|
|
n = vnorm(cross(sub(B[1], B[0]), sub(B[2], B[0])));
|
|
}
|
|
const float corr = (thickness - d) * stiffness_;
|
|
if (corr <= 0.f) continue;
|
|
|
|
const auto freeCount = [&](const int *feat, int count) {
|
|
int free = 0;
|
|
for (int k = 0; k < count; ++k)
|
|
if (!particles_[static_cast<size_t>(feat[k])].pinned) ++free;
|
|
return free;
|
|
};
|
|
const int freeA = freeCount(featA, countA);
|
|
const int freeB = freeCount(featB, countB);
|
|
if (freeA == 0 && freeB == 0) continue;
|
|
const float wA = freeA > 0 ? (freeB > 0 ? 0.5f : 1.f) : 0.f;
|
|
const float wB = freeB > 0 ? (freeA > 0 ? 0.5f : 1.f) : 0.f;
|
|
const auto pushFeature = [&](const int *feat, int count, const V3 &dir) {
|
|
for (int k = 0; k < count; ++k) {
|
|
Particle &pv = particles_[static_cast<size_t>(feat[k])];
|
|
if (pv.pinned) continue;
|
|
pv.x += dir.x;
|
|
pv.y += dir.y;
|
|
pv.z += dir.z;
|
|
}
|
|
};
|
|
if (freeA > 0) {
|
|
const V3 dir = mul(-corr * wA / float(freeA), n);
|
|
pushFeature(featA, countA, dir);
|
|
}
|
|
if (freeB > 0) {
|
|
const V3 dir = mul(corr * wB / float(freeB), n);
|
|
pushFeature(featB, countB, dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vertex-through-face: push any vertex that has crossed into the prism of a
|
|
// non-adjacent triangle back out along the face normal.
|
|
for (int i = 0; i < triCount; ++i) {
|
|
const Tri &triI = triangles_[static_cast<size_t>(i)];
|
|
const V3 A[3] = {pos[static_cast<size_t>(triI.v[0])],
|
|
pos[static_cast<size_t>(triI.v[1])],
|
|
pos[static_cast<size_t>(triI.v[2])]};
|
|
const V3 nI = vnorm(cross(sub(A[1], A[0]), sub(A[2], A[0])));
|
|
for (int j = i + 1; j < triCount; ++j) {
|
|
const Tri &triJ = triangles_[static_cast<size_t>(j)];
|
|
if (sharesVertex(triI, triJ)) continue;
|
|
const V3 B[3] = {pos[static_cast<size_t>(triJ.v[0])],
|
|
pos[static_cast<size_t>(triJ.v[1])],
|
|
pos[static_cast<size_t>(triJ.v[2])]};
|
|
const V3 nJ = vnorm(cross(sub(B[1], B[0]), sub(B[2], B[0])));
|
|
// Vertices of tri i against the face of tri j.
|
|
for (int k = 0; k < 3; ++k) {
|
|
Particle &pv = particles_[static_cast<size_t>(triI.v[k])];
|
|
if (pv.pinned) continue;
|
|
const V3 va = A[k];
|
|
const float d = dot(sub(va, B[0]), nJ);
|
|
if (std::fabs(d) >= thickness) continue;
|
|
const V3 q = add(va, mul(-d, nJ));
|
|
if (!pointInTriangle(q, B[0], B[1], B[2])) continue;
|
|
const float push = (thickness - std::fabs(d)) * stiffness_ * 0.5f;
|
|
const V3 dir = mul(d >= 0.f ? push : -push, nJ);
|
|
pv.x += dir.x;
|
|
pv.y += dir.y;
|
|
pv.z += dir.z;
|
|
}
|
|
// Vertices of tri j against the face of tri i.
|
|
for (int k = 0; k < 3; ++k) {
|
|
Particle &pv = particles_[static_cast<size_t>(triJ.v[k])];
|
|
if (pv.pinned) continue;
|
|
const V3 vb = B[k];
|
|
const float d = dot(sub(vb, A[0]), nI);
|
|
if (std::fabs(d) >= thickness) continue;
|
|
const V3 q = add(vb, mul(-d, nI));
|
|
if (!pointInTriangle(q, A[0], A[1], A[2])) continue;
|
|
const float push = (thickness - std::fabs(d)) * stiffness_ * 0.5f;
|
|
const V3 dir = mul(d >= 0.f ? push : -push, nI);
|
|
pv.x += dir.x;
|
|
pv.y += dir.y;
|
|
pv.z += dir.z;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::collideWorld(float dt) {
|
|
if (!world_ || !world_->isValid() || particleSize_ <= 0.f) return;
|
|
const float invDt = dt > 1e-6f ? 1.f / dt : 0.f;
|
|
ClothContact3D contact;
|
|
for (int i = 0; i < getParticleCount(); ++i) {
|
|
Particle &p = particles_[static_cast<size_t>(i)];
|
|
if (p.pinned) continue;
|
|
if (!world_->pointProbe(p.x, p.y, p.z, particleSize_, &contact) || !contact.hit) continue;
|
|
const float vpx = (p.x - p.px) * invDt;
|
|
const float vpy = (p.y - p.py) * invDt;
|
|
const float vpz = (p.z - p.pz) * invDt;
|
|
p.x += contact.nx * contact.depth;
|
|
p.y += contact.ny * contact.depth;
|
|
p.z += contact.nz * contact.depth;
|
|
|
|
float vbx = 0.f;
|
|
float vby = 0.f;
|
|
float vbz = 0.f;
|
|
float bodyMass = 0.f;
|
|
const bool dynamic =
|
|
contact.body != nullptr && contact.body->getType() == "dynamic";
|
|
if (dynamic) {
|
|
vbx = contact.body->getLinearVelocityX();
|
|
vby = contact.body->getLinearVelocityY();
|
|
vbz = contact.body->getLinearVelocityZ();
|
|
bodyMass = contact.body->getMass();
|
|
}
|
|
const float vn = (vpx - vbx) * contact.nx + (vpy - vby) * contact.ny +
|
|
(vpz - vbz) * contact.nz;
|
|
if (vn < 0.f) {
|
|
constexpr float restitution = 0.15f;
|
|
const float m = particleMass_;
|
|
const float reduced = bodyMass > 0.f ? (m * bodyMass) / (m + bodyMass) : m;
|
|
float j = -(1.f + restitution) * vn * reduced;
|
|
const float maxKick = 9.f; // m/s
|
|
j = std::min(j, maxKick * m);
|
|
const float kick = (j / m) * dt;
|
|
p.x += contact.nx * kick;
|
|
p.y += contact.ny * kick;
|
|
p.z += contact.nz * kick;
|
|
if (dynamic && bodyMass > 0.f) {
|
|
contact.body->applyLinearImpulse(-contact.nx * j, -contact.ny * j,
|
|
-contact.nz * j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::collideBounds() {
|
|
if (!hasBounds_) return;
|
|
const float minX = boundX_;
|
|
const float minY = boundY_;
|
|
const float minZ = boundZ_;
|
|
const float maxX = boundX_ + boundW_;
|
|
const float maxY = boundY_ + boundH_;
|
|
const float maxZ = boundZ_ + boundD_;
|
|
constexpr float bounce = 0.35f;
|
|
|
|
for (Particle &p : particles_) {
|
|
if (p.pinned) continue;
|
|
if (p.x < minX) {
|
|
const float vx = p.x - p.px;
|
|
p.x = minX;
|
|
p.px = p.x + vx * bounce;
|
|
} else if (p.x > maxX) {
|
|
const float vx = p.x - p.px;
|
|
p.x = maxX;
|
|
p.px = p.x + vx * bounce;
|
|
}
|
|
if (p.y < minY) {
|
|
const float vy = p.y - p.py;
|
|
p.y = minY;
|
|
p.py = p.y + vy * bounce;
|
|
} else if (p.y > maxY) {
|
|
const float vy = p.y - p.py;
|
|
p.y = maxY;
|
|
p.py = p.y + vy * bounce;
|
|
}
|
|
if (p.z < minZ) {
|
|
const float vz = p.z - p.pz;
|
|
p.z = minZ;
|
|
p.pz = p.z + vz * bounce;
|
|
} else if (p.z > maxZ) {
|
|
const float vz = p.z - p.pz;
|
|
p.z = maxZ;
|
|
p.pz = p.z + vz * bounce;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cloth3D::update(float dt) {
|
|
if (destroyed_) return;
|
|
if (dt < 0.f) dt = 0.f;
|
|
if (dt > 0.05f) dt = 0.05f;
|
|
|
|
updateSubsteps(dt, 2);
|
|
}
|
|
|
|
void Cloth3D::updateSubsteps(float dt, int substeps) {
|
|
if (destroyed_ || substeps < 1) return;
|
|
|
|
const float h = dt / float(substeps);
|
|
for (int s = 0; s < substeps; ++s) {
|
|
integrate(h);
|
|
solveConstraints();
|
|
solveFoldConstraint();
|
|
solveSelfCollision();
|
|
solveSelfCollisionTriangles();
|
|
collideWorld(h);
|
|
collideBounds();
|
|
if (grabIndex_ >= 0) {
|
|
Particle &p = particles_[static_cast<size_t>(grabIndex_)];
|
|
p.px = p.x;
|
|
p.py = p.y;
|
|
p.pz = p.z;
|
|
}
|
|
}
|
|
forceX_ = 0.f;
|
|
forceY_ = 0.f;
|
|
forceZ_ = 0.f;
|
|
interactStrength_ = 0.f;
|
|
}
|
|
|
|
eve::Result<void> Cloth3D::step(const eve::SimulationStep &stepValue, const SimulationSettings &settings) {
|
|
if (destroyed_)
|
|
return eve::Result<void>::failure(eve::Diagnostic::error(
|
|
eve::DiagnosticCode::PreconditionViolation, "Cannot step a destroyed cloth", "physics.cloth3d.step"));
|
|
auto valid = detail::validateSimulationStep(stepValue, settings, observation_);
|
|
if (!valid) return valid;
|
|
auto next = detail::advanceSimulationObservation(observation_, stepValue);
|
|
if (!next) return eve::Result<void>::failure(next.status());
|
|
try {
|
|
updateSubsteps(static_cast<float>(stepValue.delta.seconds()), settings.subStepCount);
|
|
} catch (const std::exception &error) {
|
|
return eve::Result<void>::failure(eve::Diagnostic::error(
|
|
eve::DiagnosticCode::Failed, std::string("Cloth3D step failed: ") + error.what(), "physics.cloth3d.step"));
|
|
} catch (...) {
|
|
return eve::Result<void>::failure(eve::Diagnostic::error(
|
|
eve::DiagnosticCode::Failed, "Cloth3D step failed with an unknown exception", "physics.cloth3d.step"));
|
|
}
|
|
observation_ = std::move(next).takeValue();
|
|
return eve::Result<void>::success(eve::Status::success(eve::StatusCode::Applied));
|
|
}
|
|
|
|
eve::Result<void> Cloth3D::restoreObservation(const SimulationObservation &observation) {
|
|
auto valid = detail::validateSimulationObservation(observation, "physics.cloth3d.restoreObservation");
|
|
if (!valid) return valid;
|
|
if (destroyed_)
|
|
return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation,
|
|
"Cannot restore a destroyed cloth",
|
|
"physics.cloth3d.restoreObservation"));
|
|
observation_ = observation;
|
|
return eve::Result<void>::success(eve::Status::success(eve::StatusCode::Applied));
|
|
}
|
|
|
|
void Cloth3D::draw(graphics::Graphics *gfx) {
|
|
if (!gfx || destroyed_ || getParticleCount() < 4) return;
|
|
|
|
// Double-sided mesh: front vertices carry the face normals, back vertices
|
|
// duplicate positions with flipped normals and reversed winding.
|
|
const int gridVertices = cols_ * rows_;
|
|
const int vertexCount = gridVertices * 2;
|
|
const int quadCount = (cols_ - 1) * (rows_ - 1);
|
|
const int indexCount = quadCount * 12;
|
|
|
|
std::vector<float> pos(static_cast<size_t>(vertexCount) * 3);
|
|
std::vector<float> nrm(static_cast<size_t>(vertexCount) * 3, 0.f);
|
|
std::vector<float> uv(static_cast<size_t>(vertexCount) * 2);
|
|
for (int r = 0; r < rows_; ++r) {
|
|
for (int c = 0; c < cols_; ++c) {
|
|
const size_t i = static_cast<size_t>(r * cols_ + c);
|
|
const Particle &p = particles_[i];
|
|
const float u = cols_ > 1 ? float(c) / float(cols_ - 1) : 0.f;
|
|
const float v = rows_ > 1 ? float(r) / float(rows_ - 1) : 0.f;
|
|
for (int side = 0; side < 2; ++side) {
|
|
const size_t vi = i + static_cast<size_t>(side) * gridVertices;
|
|
pos[vi * 3 + 0] = p.x;
|
|
pos[vi * 3 + 1] = p.y;
|
|
pos[vi * 3 + 2] = p.z;
|
|
uv[vi * 2 + 0] = u;
|
|
uv[vi * 2 + 1] = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Accumulate face normals.
|
|
for (const Tri &tri : triangles_) {
|
|
const float *pa = &pos[static_cast<size_t>(tri.v[0]) * 3];
|
|
const float *pb = &pos[static_cast<size_t>(tri.v[1]) * 3];
|
|
const float *pc = &pos[static_cast<size_t>(tri.v[2]) * 3];
|
|
float e1x = pb[0] - pa[0], e1y = pb[1] - pa[1], e1z = pb[2] - pa[2];
|
|
float e2x = pc[0] - pa[0], e2y = pc[1] - pa[1], e2z = pc[2] - pa[2];
|
|
float nx = e1y * e2z - e1z * e2y;
|
|
float ny = e1z * e2x - e1x * e2z;
|
|
float nz = e1x * e2y - e1y * e2x;
|
|
const float len = std::sqrt(nx * nx + ny * ny + nz * nz);
|
|
if (len > 1e-8f) {
|
|
nx /= len; ny /= len; nz /= len;
|
|
} else {
|
|
nx = 0.f; ny = 1.f; nz = 0.f;
|
|
}
|
|
for (int v : tri.v) {
|
|
nrm[static_cast<size_t>(v) * 3 + 0] += nx;
|
|
nrm[static_cast<size_t>(v) * 3 + 1] += ny;
|
|
nrm[static_cast<size_t>(v) * 3 + 2] += nz;
|
|
nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 0] -= nx;
|
|
nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 1] -= ny;
|
|
nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 2] -= nz;
|
|
}
|
|
}
|
|
for (int i = 0; i < gridVertices; ++i) {
|
|
const float nx = nrm[static_cast<size_t>(i) * 3 + 0];
|
|
const float ny = nrm[static_cast<size_t>(i) * 3 + 1];
|
|
const float nz = nrm[static_cast<size_t>(i) * 3 + 2];
|
|
const float len = std::sqrt(nx * nx + ny * ny + nz * nz);
|
|
for (int side = 0; side < 2; ++side) {
|
|
const size_t vi = static_cast<size_t>(i) + static_cast<size_t>(side) * gridVertices;
|
|
const float sign = side == 0 ? 1.f : -1.f;
|
|
if (len > 1e-8f) {
|
|
nrm[vi * 3 + 0] = sign * nx / len;
|
|
nrm[vi * 3 + 1] = sign * ny / len;
|
|
nrm[vi * 3 + 2] = sign * nz / len;
|
|
} else {
|
|
nrm[vi * 3 + 1] = sign;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mesh_ || meshVertexCount_ != vertexCount || meshIndexCount_ != indexCount) {
|
|
std::vector<uint32_t> indices(static_cast<size_t>(indexCount));
|
|
int out = 0;
|
|
for (int r = 0; r + 1 < rows_; ++r) {
|
|
for (int c = 0; c + 1 < cols_; ++c) {
|
|
const uint32_t a = static_cast<uint32_t>(r * cols_ + c);
|
|
const uint32_t b = static_cast<uint32_t>(r * cols_ + c + 1);
|
|
const uint32_t cc = static_cast<uint32_t>((r + 1) * cols_ + c + 1);
|
|
const uint32_t d = static_cast<uint32_t>((r + 1) * cols_ + c);
|
|
// Front faces (CCW when viewed from +Y).
|
|
indices[static_cast<size_t>(out) + 0] = a;
|
|
indices[static_cast<size_t>(out) + 1] = d;
|
|
indices[static_cast<size_t>(out) + 2] = b;
|
|
indices[static_cast<size_t>(out) + 3] = d;
|
|
indices[static_cast<size_t>(out) + 4] = cc;
|
|
indices[static_cast<size_t>(out) + 5] = b;
|
|
// Back faces (reversed winding on the duplicated vertex set).
|
|
const uint32_t oa = a + static_cast<uint32_t>(gridVertices);
|
|
const uint32_t ob = b + static_cast<uint32_t>(gridVertices);
|
|
const uint32_t occ = cc + static_cast<uint32_t>(gridVertices);
|
|
const uint32_t od = d + static_cast<uint32_t>(gridVertices);
|
|
indices[static_cast<size_t>(out) + 6] = ob;
|
|
indices[static_cast<size_t>(out) + 7] = od;
|
|
indices[static_cast<size_t>(out) + 8] = oa;
|
|
indices[static_cast<size_t>(out) + 9] = ob;
|
|
indices[static_cast<size_t>(out) + 10] = occ;
|
|
indices[static_cast<size_t>(out) + 11] = od;
|
|
out += 12;
|
|
}
|
|
}
|
|
mesh_ = gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), vertexCount,
|
|
indices.data(), indexCount);
|
|
meshVertexCount_ = vertexCount;
|
|
meshIndexCount_ = indexCount;
|
|
} else {
|
|
gfx->updateMeshVertices(mesh_, pos.data(), nrm.data(), uv.data(), vertexCount, nullptr, 0);
|
|
}
|
|
|
|
if (mesh_) {
|
|
gfx->drawMesh(mesh_, glm::mat4(1.f), nullptr,
|
|
Color(colorR_, colorG_, colorB_, colorA_));
|
|
}
|
|
}
|
|
|
|
} // namespace eve::physics
|