#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 #include #include #include #include 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::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(cols_ * rows_)); for (int r = 0; r < rows_; ++r) { for (int c = 0; c < cols_; ++c) { Particle &p = particles_[static_cast(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(cols_ * rows_)); for (int r = 0; r < rows_; ++r) { for (int c = 0; c < cols_; ++c) { Particle &p = particles_[static_cast(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(a)]; const Particle &pb = particles_[static_cast(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> edgeTris; auto addTri = [&](int a, int b, int c, std::unordered_map> &map) { Tri tri; tri.v[0] = a; tri.v[1] = b; tri.v[2] = c; const int idx = static_cast(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(entry.second[0])]; const Tri &t1 = triangles_[static_cast(entry.second[1])]; const int e0 = int(entry.first >> 32); const int e1 = static_cast(static_cast(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(index)].pinned = true; } void Cloth3D::unpin(int index) { if (!validIndex(index)) throw Exception("Cloth3D.unpin: index out of range"); particles_[static_cast(index)].pinned = false; } void Cloth3D::pinTopRow() { for (int c = 0; c < cols_; ++c) particles_[static_cast(c)].pinned = true; } bool Cloth3D::isPinned(int index) const { if (!validIndex(index)) return false; return particles_[static_cast(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(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(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(index)].x; } float Cloth3D::getParticleY(int index) const { if (!validIndex(index)) return 0.f; return particles_[static_cast(index)].y; } float Cloth3D::getParticleZ(int index) const { if (!validIndex(index)) return 0.f; return particles_[static_cast(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(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(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(link.a)]; Particle &b = particles_[static_cast(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(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(pair.a)]; Particle &pb = particles_[static_cast(pair.b)]; Particle &pk = particles_[static_cast(pair.k)]; Particle &pl = particles_[static_cast(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(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(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(triangles_.size()); std::vector 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> 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(ti)]; V3 mn = pos[static_cast(tri.v[0])]; V3 mx = mn; for (int k = 1; k < 3; ++k) { const V3 &p = pos[static_cast(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 processed; std::vector cells; int featA[3]; int featB[3]; for (int i = 0; i < triCount; ++i) { const Tri &triI = triangles_[static_cast(i)]; const V3 &pa0 = pos[static_cast(triI.v[0])]; V3 mn = pa0; V3 mx = mn; for (int k = 1; k < 3; ++k) { const V3 &p = pos[static_cast(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(j)]; if (sharesVertex(triI, triJ)) continue; const V3 A[3] = {pos[static_cast(triI.v[0])], pos[static_cast(triI.v[1])], pos[static_cast(triI.v[2])]}; const V3 B[3] = {pos[static_cast(triJ.v[0])], pos[static_cast(triJ.v[1])], pos[static_cast(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(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(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(i)]; const V3 A[3] = {pos[static_cast(triI.v[0])], pos[static_cast(triI.v[1])], pos[static_cast(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(j)]; if (sharesVertex(triI, triJ)) continue; const V3 B[3] = {pos[static_cast(triJ.v[0])], pos[static_cast(triJ.v[1])], pos[static_cast(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(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(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(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(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 Cloth3D::step(const eve::SimulationStep &stepValue, const SimulationSettings &settings) { if (destroyed_) return eve::Result::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::failure(next.status()); try { updateSubsteps(static_cast(stepValue.delta.seconds()), settings.subStepCount); } catch (const std::exception &error) { return eve::Result::failure(eve::Diagnostic::error( eve::DiagnosticCode::Failed, std::string("Cloth3D step failed: ") + error.what(), "physics.cloth3d.step")); } catch (...) { return eve::Result::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::success(eve::Status::success(eve::StatusCode::Applied)); } eve::Result Cloth3D::restoreObservation(const SimulationObservation &observation) { auto valid = detail::validateSimulationObservation(observation, "physics.cloth3d.restoreObservation"); if (!valid) return valid; if (destroyed_) return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "Cannot restore a destroyed cloth", "physics.cloth3d.restoreObservation")); observation_ = observation; return eve::Result::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 pos(static_cast(vertexCount) * 3); std::vector nrm(static_cast(vertexCount) * 3, 0.f); std::vector uv(static_cast(vertexCount) * 2); for (int r = 0; r < rows_; ++r) { for (int c = 0; c < cols_; ++c) { const size_t i = static_cast(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(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(tri.v[0]) * 3]; const float *pb = &pos[static_cast(tri.v[1]) * 3]; const float *pc = &pos[static_cast(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(v) * 3 + 0] += nx; nrm[static_cast(v) * 3 + 1] += ny; nrm[static_cast(v) * 3 + 2] += nz; nrm[(static_cast(v) + gridVertices) * 3 + 0] -= nx; nrm[(static_cast(v) + gridVertices) * 3 + 1] -= ny; nrm[(static_cast(v) + gridVertices) * 3 + 2] -= nz; } } for (int i = 0; i < gridVertices; ++i) { const float nx = nrm[static_cast(i) * 3 + 0]; const float ny = nrm[static_cast(i) * 3 + 1]; const float nz = nrm[static_cast(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(i) + static_cast(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 indices(static_cast(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(r * cols_ + c); const uint32_t b = static_cast(r * cols_ + c + 1); const uint32_t cc = static_cast((r + 1) * cols_ + c + 1); const uint32_t d = static_cast((r + 1) * cols_ + c); // Front faces (CCW when viewed from +Y). indices[static_cast(out) + 0] = a; indices[static_cast(out) + 1] = d; indices[static_cast(out) + 2] = b; indices[static_cast(out) + 3] = d; indices[static_cast(out) + 4] = cc; indices[static_cast(out) + 5] = b; // Back faces (reversed winding on the duplicated vertex set). const uint32_t oa = a + static_cast(gridVertices); const uint32_t ob = b + static_cast(gridVertices); const uint32_t occ = cc + static_cast(gridVertices); const uint32_t od = d + static_cast(gridVertices); indices[static_cast(out) + 6] = ob; indices[static_cast(out) + 7] = od; indices[static_cast(out) + 8] = oa; indices[static_cast(out) + 9] = ob; indices[static_cast(out) + 10] = occ; indices[static_cast(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