载入中...
搜索中...
未找到
Cloth3D.cpp
浏览该文件的文档.
3
4#include "common/Exception.h"
5#include "graphics/Canvas.h"
6#include "graphics/Graphics.h"
7#include "graphics/Mesh.h"
8#include "physics/Body3D.h"
9#include "physics/World3D.h"
10
11#include <algorithm>
12#include <cmath>
13#include <exception>
14#include <limits>
15#include <utility>
16
17namespace eve::physics {
18
19// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
22
23namespace {
24constexpr float kPi = 3.14159265358979323846f;
25
26int64_t pairKey(int lo, int hi) { return (int64_t(lo) << 32) | int64_t(hi); }
27
28// Minimal 3D vector helpers for the triangle-level self-collision pass.
29struct V3 {
30 float x = 0.f, y = 0.f, z = 0.f;
31};
32
33V3 sub(const V3& a, const V3& b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
34V3 add(const V3& a, const V3& b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
35V3 mul(float s, const V3& a) { return {s * a.x, s * a.y, s * a.z}; }
36V3 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}; }
37float dot(const V3& a, const V3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
38V3 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}; }
39float vlen(const V3& a) { return std::sqrt(dot(a, a)); }
40V3 vnorm(const V3& a) {
41 const float l = vlen(a);
42 return l > 1e-8f ? mul(1.f / l, a) : V3{0.f, 1.f, 0.f};
43}
44
45// Rotate vector v around the given unit axis by angle (right-hand rule).
46V3 rotateAround(const V3& v, const V3& axis, float angle) {
47 const float c = std::cos(angle);
48 const float sn = std::sin(angle);
49 const V3 cv = cross(axis, v);
50 const float d = dot(axis, v);
51 return add(add(mul(c, v), mul(sn, cv)), mul(d * (1.f - c), axis));
52}
53
54// Signed angle from a to b around the unit axis u (positive = CCW along +u).
55float signedAngle(const V3& a, const V3& b, const V3& u) { return std::atan2(dot(cross(a, b), u), dot(a, b)); }
56
57// Closest point on segment [a,b] to point p (Ericson, RTCD ch. 5).
58V3 closestPointSegment(const V3& a, const V3& b, const V3& p) {
59 const V3 ab = sub(b, a);
60 const float len2 = dot(ab, ab);
61 if (len2 < 1e-12f) return a;
62 const float t = std::clamp(dot(sub(p, a), ab) / len2, 0.f, 1.f);
63 return fma(ab, t, a);
64}
65
66// Closest point on triangle (a,b,c) to point p (Ericson, RTCD ch. 5).
67V3 closestPointTriangle(const V3& a, const V3& b, const V3& c, const V3& p) {
68 const V3 ab = sub(b, a);
69 const V3 ac = sub(c, a);
70 const V3 ap = sub(p, a);
71 const float d1 = dot(ab, ap);
72 const float d2 = dot(ac, ap);
73 if (d1 <= 0.f && d2 <= 0.f) return a;
74
75 const V3 bp = sub(p, b);
76 const float d3 = dot(ab, bp);
77 const float d4 = dot(ac, bp);
78 if (d3 >= 0.f && d4 <= d3) return b;
79
80 const float vc = d1 * d4 - d3 * d2;
81 if (vc <= 0.f && d1 >= 0.f && d3 <= 0.f) {
82 const float t = d1 / (d1 - d3);
83 return fma(ab, t, a);
84 }
85
86 const V3 cp = sub(p, c);
87 const float d5 = dot(ab, cp);
88 const float d6 = dot(ac, cp);
89 if (d6 >= 0.f && d5 <= d6) return c;
90
91 const float vb = d5 * d2 - d1 * d6;
92 if (vb <= 0.f && d2 >= 0.f && d6 <= 0.f) {
93 const float t = d2 / (d2 - d6);
94 return fma(ac, t, a);
95 }
96
97 const float va = d3 * d6 - d5 * d4;
98 if (va <= 0.f && (d4 - d3) >= 0.f && (d5 - d6) >= 0.f) {
99 const float t = (d4 - d3) / ((d4 - d3) + (d5 - d6));
100 return fma(sub(c, b), t, b);
101 }
102
103 const float denom = 1.f / (va + vb + vc);
104 const float v = vb * denom;
105 const float w = vc * denom;
106 return add(fma(ab, v, a), mul(w, ac));
107}
108
109// Closest points between segments (p1,p2) and (q1,q2); returns distance.
110float closestPointSegments(const V3& p1, const V3& p2, const V3& q1, const V3& q2, V3& cp1, V3& cp2) {
111 const V3 d1 = sub(p2, p1);
112 const V3 d2 = sub(q2, q1);
113 const V3 r = sub(p1, q1);
114 const float a = dot(d1, d1);
115 const float e = dot(d2, d2);
116 const float f = dot(d2, r);
117 float s = 0.f;
118 float t = 0.f;
119 if (a <= 1e-12f && e <= 1e-12f) {
120 s = t = 0.f;
121 } else if (a <= 1e-12f) {
122 s = 0.f;
123 t = std::clamp(f / e, 0.f, 1.f);
124 } else {
125 const float c = dot(d1, r);
126 if (e <= 1e-12f) {
127 t = 0.f;
128 s = std::clamp(-c / a, 0.f, 1.f);
129 } else {
130 const float b = dot(d1, d2);
131 const float denom = a * e - b * b;
132 s = denom > 1e-12f ? std::clamp((b * f - c * e) / denom, 0.f, 1.f) : 0.f;
133 t = (b * s + f) / e;
134 if (t < 0.f) {
135 t = 0.f;
136 s = std::clamp(-c / a, 0.f, 1.f);
137 } else if (t > 1.f) {
138 t = 1.f;
139 s = std::clamp((b - c) / a, 0.f, 1.f);
140 }
141 }
142 }
143 cp1 = fma(d1, s, p1);
144 cp2 = fma(d2, t, q1);
145 return vlen(sub(cp1, cp2));
146}
147
148bool pointInTriangle(const V3& p, const V3& a, const V3& b, const V3& c) {
149 const V3 n = cross(sub(b, a), sub(c, a));
150 const V3 pa = cross(sub(b, a), sub(p, a));
151 const V3 pb = cross(sub(c, b), sub(p, b));
152 const V3 pc = cross(sub(a, c), sub(p, c));
153 return dot(pa, n) >= -1e-8f && dot(pb, n) >= -1e-8f && dot(pc, n) >= -1e-8f;
154}
155
156// Closest distance between two triangles plus the indices of the vertices
157// forming the closest feature on each side (1, 2 or 3 vertices).
158float triangleDistance(const V3* A, const V3* B, int featA[3], int featB[3], int& countA, int& countB, V3& cpA,
159 V3& cpB) {
160 float best = std::numeric_limits<float>::max();
161 countA = countB = 0;
162 for (int i = 0; i < 3; ++i) {
163 const V3 cp = closestPointTriangle(B[0], B[1], B[2], A[i]);
164 const float d = vlen(sub(A[i], cp));
165 if (d < best) {
166 best = d;
167 cpA = A[i];
168 cpB = cp;
169 featA[0] = i;
170 featB[0] = 0;
171 featB[1] = 1;
172 featB[2] = 2;
173 countA = 1;
174 countB = 3;
175 }
176 }
177 for (int j = 0; j < 3; ++j) {
178 const V3 cp = closestPointTriangle(A[0], A[1], A[2], B[j]);
179 const float d = vlen(sub(B[j], cp));
180 if (d < best) {
181 best = d;
182 cpA = cp;
183 cpB = B[j];
184 featA[0] = 0;
185 featA[1] = 1;
186 featA[2] = 2;
187 featB[0] = j;
188 countA = 3;
189 countB = 1;
190 }
191 }
192 const int aEdges[3][2] = {{0, 1}, {1, 2}, {2, 0}};
193 const int bEdges[3][2] = {{0, 1}, {1, 2}, {2, 0}};
194 for (const auto& ea : aEdges) {
195 for (const auto& eb : bEdges) {
196 V3 cp1;
197 V3 cp2;
198 const float d = closestPointSegments(A[ea[0]], A[ea[1]], B[eb[0]], B[eb[1]], cp1, cp2);
199 if (d < best) {
200 best = d;
201 cpA = cp1;
202 cpB = cp2;
203 featA[0] = ea[0];
204 featA[1] = ea[1];
205 featB[0] = eb[0];
206 featB[1] = eb[1];
207 countA = 2;
208 countB = 2;
209 }
210 }
211 }
212 return best;
213}
214} // namespace
215
216Cloth3D::Cloth3D(int cols, int rows, float spacing, float originX, float originY, float originZ)
217 : cols_(cols), rows_(rows), spacing_(spacing), originX_(originX), originY_(originY), originZ_(originZ) {
218 auto model = ClothModel::grid(cols, rows, spacing, originX, originY, originZ);
219 if (!model)
220 throw Exception("%s", model.error() ? model.error()->message().c_str() : "Cloth3D grid model creation failed");
221 initializeFromModel(model.value());
222}
223
224Cloth3D::Cloth3D(const ClothModel& model) { initializeFromModel(model); }
225
227
228void Cloth3D::destroy() { destroyed_ = true; }
229
231 particles_ = restParticles_;
232 grabIndex_ = -1;
233 forceX_ = forceY_ = forceZ_ = 0.f;
234 interactStrength_ = 0.f;
235}
236
237void Cloth3D::rebuildLinks() {
238 links_.clear();
239 auto add = [&](int a, int b) {
240 if (a < 0 || b < 0 || a >= getParticleCount() || b >= getParticleCount()) return;
241 Link link;
242 link.a = a;
243 link.b = b;
244 const Particle& pa = particles_[static_cast<size_t>(a)];
245 const Particle& pb = particles_[static_cast<size_t>(b)];
246 const float dx = pb.x - pa.x;
247 const float dy = pb.y - pa.y;
248 const float dz = pb.z - pa.z;
249 link.rest = std::sqrt(dx * dx + dy * dy + dz * dz);
250 if (link.rest > 1e-4f) links_.push_back(link);
251 };
252
253 for (int r = 0; r < rows_; ++r) {
254 for (int c = 0; c < cols_; ++c) {
255 const int i = r * cols_ + c;
256 // Structural
257 if (c + 1 < cols_) add(i, i + 1);
258 if (r + 1 < rows_) add(i, i + cols_);
259 // Shear
260 if (c + 1 < cols_ && r + 1 < rows_) add(i, i + cols_ + 1);
261 if (c > 0 && r + 1 < rows_) add(i, i + cols_ - 1);
262 // Bend (every other)
263 if (c + 2 < cols_) add(i, i + 2);
264 if (r + 2 < rows_) add(i, i + cols_ * 2);
265 }
266 }
267 buildLinkKeys();
268}
269
270void Cloth3D::buildLinkKeys() {
271 linkKeys_.clear();
272 for (const Link& link : links_) {
273 const int lo = std::min(link.a, link.b);
274 const int hi = std::max(link.a, link.b);
275 linkKeys_.insert(pairKey(lo, hi));
276 }
277}
278
279bool Cloth3D::areLinked(int a, int b) const {
280 if (a == b) return true;
281 const int lo = std::min(a, b);
282 const int hi = std::max(a, b);
283 return linkKeys_.find(pairKey(lo, hi)) != linkKeys_.end();
284}
285
286void Cloth3D::rebuildTriangles() {
287 triangles_.clear();
288 foldPairs_.clear();
289
290 // Two triangles per quad, wound so the +Y side is the front face.
291 std::unordered_map<int64_t, std::vector<int>> edgeTris;
292 auto addTri = [&](int a, int b, int c, std::unordered_map<int64_t, std::vector<int>>& map) {
293 Tri tri;
294 tri.v[0] = a;
295 tri.v[1] = b;
296 tri.v[2] = c;
297 const int idx = static_cast<int>(triangles_.size());
298 const int edges[3][2] = {{a, b}, {b, c}, {c, a}};
299 for (const auto& e : edges) {
300 map[pairKey(std::min(e[0], e[1]), std::max(e[0], e[1]))].push_back(idx);
301 }
302 triangles_.push_back(tri);
303 };
304
305 for (int r = 0; r + 1 < rows_; ++r) {
306 for (int c = 0; c + 1 < cols_; ++c) {
307 const int a = r * cols_ + c;
308 const int b = r * cols_ + c + 1;
309 const int cc = (r + 1) * cols_ + c + 1;
310 const int d = (r + 1) * cols_ + c;
311 addTri(a, d, b, edgeTris);
312 addTri(d, cc, b, edgeTris);
313 }
314 }
315
316 for (const auto& entry : edgeTris) {
317 if (entry.second.size() != 2) continue;
318 const Tri& t0 = triangles_[static_cast<size_t>(entry.second[0])];
319 const Tri& t1 = triangles_[static_cast<size_t>(entry.second[1])];
320 const int e0 = int(entry.first >> 32);
321 const int e1 = static_cast<int>(static_cast<uint32_t>(entry.first));
322 // Shared edge endpoints e0/e1; opposite vertices are the third of each tri.
323 int k = -1;
324 for (int v : t0.v) {
325 if (v != e0 && v != e1) {
326 k = v;
327 break;
328 }
329 }
330 int l = -1;
331 for (int v : t1.v) {
332 if (v != e0 && v != e1) {
333 l = v;
334 break;
335 }
336 }
337 if (k < 0 || l < 0) continue;
338 FoldPair pair;
339 pair.a = e0;
340 pair.b = e1;
341 pair.k = k;
342 pair.l = l;
343 foldPairs_.push_back(pair);
344 }
345}
346
347bool Cloth3D::validIndex(int index) const { return index >= 0 && index < getParticleCount(); }
348
349void Cloth3D::setGravity(float gx, float gy, float gz) {
350 gravityX_ = gx;
351 gravityY_ = gy;
352 gravityZ_ = gz;
353}
354
355void Cloth3D::setStiffness(float stiffness) { stiffness_ = std::clamp(stiffness, 0.f, 1.f); }
356
357void Cloth3D::setStretchCompliance(float compliance) {
358 stretchCompliance_ = std::isfinite(compliance) ? std::max(0.f, compliance) : 0.f;
359}
360
361void Cloth3D::setShearCompliance(float compliance) {
362 shearCompliance_ = std::isfinite(compliance) ? std::max(0.f, compliance) : 0.f;
363}
364
365void Cloth3D::setBendCompliance(float compliance) {
366 bendCompliance_ = std::isfinite(compliance) ? std::max(0.f, compliance) : 0.f;
367}
368
369void Cloth3D::setIterations(int iterations) { iterations_ = std::max(1, iterations); }
370
371void Cloth3D::setDamping(float damping) { damping_ = std::clamp(damping, 0.f, 1.f); }
372
373void Cloth3D::setParticleSize(float size) { particleSize_ = std::max(0.01f, size); }
374
375void Cloth3D::setParticleMass(float mass) { particleMass_ = std::max(1e-4f, mass); }
376
377void Cloth3D::initializeFromModel(const ClothModel& model) {
378 cols_ = model.gridCols();
379 rows_ = model.gridRows();
380 spacing_ = model.gridSpacing();
381 if (!model.particles().empty()) {
382 originX_ = model.particles().front().x;
383 originY_ = model.particles().front().y;
384 originZ_ = model.particles().front().z;
385 }
386 particles_.clear();
387 particles_.reserve(model.particles().size());
388 for (const ClothModelParticle& source : model.particles()) {
389 Particle particle;
390 particle.x = particle.px = source.x;
391 particle.y = particle.py = source.y;
392 particle.z = particle.pz = source.z;
393 particle.inverseMass = source.inverseMass;
394 particle.pinned = source.inverseMass == 0.f;
395 particles_.push_back(particle);
396 }
397 restParticles_ = particles_;
398
399 links_.clear();
400 links_.reserve(model.distanceConstraints().size());
401 for (const ClothModelDistanceConstraint& source : model.distanceConstraints())
402 links_.push_back({source.a, source.b, source.restLength, source.kind, 0.f});
403
404 triangles_.clear();
405 triangles_.reserve(model.triangles().size());
406 for (const ClothModelTriangle& source : model.triangles())
407 triangles_.push_back({{source.vertices[0], source.vertices[1], source.vertices[2]}});
408
409 foldPairs_.clear();
410 foldPairs_.reserve(model.foldConstraints().size());
411 for (const ClothModelFoldConstraint& source : model.foldConstraints())
412 foldPairs_.push_back({source.a, source.b, source.oppositeA, source.oppositeB});
413 tethers_.clear();
414 tethers_.reserve(model.tetherConstraints().size());
415 for (const ClothModelTetherConstraint& source : model.tetherConstraints())
416 tethers_.push_back({source.particle, source.anchor, source.maxLength, 0.f});
417 restVolume_ = model.restVolume();
418 buildLinkKeys();
419}
420
421void Cloth3D::setWindVelocity(float vx, float vy, float vz) {
422 windVelocityX_ = vx;
423 windVelocityY_ = vy;
424 windVelocityZ_ = vz;
425}
426
427float Cloth3D::getWindVelocityX() const { return windVelocityX_; }
428
429float Cloth3D::getWindVelocityY() const { return windVelocityY_; }
430
431float Cloth3D::getWindVelocityZ() const { return windVelocityZ_; }
432
433void Cloth3D::setAerodynamics(float airDensity, float dragCoefficient, float liftCoefficient) {
434 airDensity_ = std::max(0.f, airDensity);
435 dragCoefficient_ = std::max(0.f, dragCoefficient);
436 liftCoefficient_ = std::max(0.f, liftCoefficient);
437}
438
439float Cloth3D::getAirDensity() const { return airDensity_; }
440
441float Cloth3D::getDragCoefficient() const { return dragCoefficient_; }
442
443float Cloth3D::getLiftCoefficient() const { return liftCoefficient_; }
444
445void Cloth3D::setSelfCollision(bool on) { selfCollision_ = on; }
446
447void Cloth3D::setFoldStiffness(float k) { foldStiffness_ = std::clamp(k, 0.f, 1.f); }
448
449void Cloth3D::setMaxFoldAngle(float degrees) { maxFoldAngle_ = std::clamp(degrees, 0.f, 180.f) * kPi / 180.f; }
450
451void Cloth3D::setBounds(float x, float y, float z, float w, float h, float d) {
452 if (w <= 0.f || h <= 0.f || d <= 0.f) {
453 clearBounds();
454 return;
455 }
456 hasBounds_ = true;
457 boundX_ = x;
458 boundY_ = y;
459 boundZ_ = z;
460 boundW_ = w;
461 boundH_ = h;
462 boundD_ = d;
463}
464
465void Cloth3D::clearBounds() { hasBounds_ = false; }
466
467void Cloth3D::setCollisionMaterial(float friction, float restitution) {
468 collisionFriction_ = std::isfinite(friction) ? std::clamp(friction, 0.f, 1.f) : 0.f;
469 collisionRestitution_ = std::isfinite(restitution) ? std::clamp(restitution, 0.f, 1.f) : 0.f;
470}
471
472void Cloth3D::setCollisionFilter(uint64_t categoryBits, uint64_t maskBits) {
473 collisionCategoryBits_ = categoryBits;
474 collisionMaskBits_ = maskBits;
475}
476
478 if (!validIndex(index)) throw Exception("Cloth3D.pin: index out of range");
479 Particle& particle = particles_[static_cast<size_t>(index)];
480 particle.pinned = true;
481 particle.inverseMass = 0.f;
482}
483
485 if (!validIndex(index)) throw Exception("Cloth3D.unpin: index out of range");
486 Particle& particle = particles_[static_cast<size_t>(index)];
487 particle.pinned = false;
488 particle.inverseMass = 1.f;
489}
490
492 for (int c = 0; c < cols_; ++c) pin(c);
493}
494
495bool Cloth3D::isPinned(int index) const {
496 if (!validIndex(index)) return false;
497 return particles_[static_cast<size_t>(index)].pinned;
498}
499
500void Cloth3D::setParticleInverseMass(int index, float inverseMass) {
501 if (!validIndex(index)) throw Exception("Cloth3D.setParticleInverseMass: index out of range");
502 Particle& particle = particles_[static_cast<size_t>(index)];
503 particle.inverseMass = std::isfinite(inverseMass) ? std::max(0.f, inverseMass) : 0.f;
504 particle.pinned = particle.inverseMass == 0.f;
505}
506
508 return validIndex(index) ? particles_[static_cast<size_t>(index)].inverseMass : 0.f;
509}
510
511int Cloth3D::grabAt(float x, float y, float z, float radius) {
512 grabIndex_ = -1;
513 float best = radius * radius;
514 for (int i = 0; i < getParticleCount(); ++i) {
515 const Particle& p = particles_[static_cast<size_t>(i)];
516 if (p.pinned) continue;
517 const float dx = p.x - x;
518 const float dy = p.y - y;
519 const float dz = p.z - z;
520 const float d2 = dx * dx + dy * dy + dz * dz;
521 if (d2 <= best) {
522 best = d2;
523 grabIndex_ = i;
524 }
525 }
526 if (grabIndex_ >= 0) moveGrab(x, y, z);
527 return grabIndex_;
528}
529
530void Cloth3D::moveGrab(float x, float y, float z) {
531 grabX_ = x;
532 grabY_ = y;
533 grabZ_ = z;
534 if (!validIndex(grabIndex_)) return;
535 Particle& p = particles_[static_cast<size_t>(grabIndex_)];
536 p.x = x;
537 p.y = y;
538 p.z = z;
539 p.px = x;
540 p.py = y;
541 p.pz = z;
542}
543
544void Cloth3D::releaseGrab() { grabIndex_ = -1; }
545
546void Cloth3D::applyForce(float fx, float fy, float fz) {
547 forceX_ += fx;
548 forceY_ += fy;
549 forceZ_ += fz;
550}
551
552void Cloth3D::interactAt(float x, float y, float z, float radius, float strength) {
553 interactX_ = x;
554 interactY_ = y;
555 interactZ_ = z;
556 interactRadius_ = std::max(0.f, radius);
557 interactStrength_ = strength;
558}
559
561
562void Cloth3D::setColor(float r, float g, float b, float a) {
563 colorR_ = r;
564 colorG_ = g;
565 colorB_ = b;
566 colorA_ = a;
567}
568
569float Cloth3D::getParticleX(int index) const {
570 if (!validIndex(index)) return 0.f;
571 return particles_[static_cast<size_t>(index)].x;
572}
573
574float Cloth3D::getParticleY(int index) const {
575 if (!validIndex(index)) return 0.f;
576 return particles_[static_cast<size_t>(index)].y;
577}
578
579float Cloth3D::getParticleZ(int index) const {
580 if (!validIndex(index)) return 0.f;
581 return particles_[static_cast<size_t>(index)].z;
582}
583
584void Cloth3D::setParticlePosition(int index, float x, float y, float z) {
585 if (!validIndex(index)) throw Exception("Cloth3D.setParticlePosition: index out of range");
586 Particle& p = particles_[static_cast<size_t>(index)];
587 p.x = p.px = x;
588 p.y = p.py = y;
589 p.z = p.pz = z;
590}
591
592int64_t Cloth3D::cellKey(int cx, int cy, int cz) const {
593 // 21 bits per axis keeps the packed key inside a signed 64-bit int.
594 return int64_t(uint32_t(cx) & 0x1fffffu) | (int64_t(uint32_t(cy) & 0x1fffffu) << 21) |
595 (int64_t(uint32_t(cz) & 0x1fffffu) << 42);
596}
597
598void Cloth3D::rebuildHash() {
599 hash_.clear();
600 const float cell = std::max(1e-3f, particleSize_ * 2.f);
601 const float inv = 1.f / cell;
602 for (int i = 0; i < getParticleCount(); ++i) {
603 const Particle& p = particles_[static_cast<size_t>(i)];
604 const int cx = int(std::floor(p.x * inv));
605 const int cy = int(std::floor(p.y * inv));
606 const int cz = int(std::floor(p.z * inv));
607 hash_[cellKey(cx, cy, cz)].push_back(i);
608 }
609}
610
611void Cloth3D::integrate(float dt) {
612 if (dt <= 0.f) return;
613 const float ax = gravityX_ + forceX_;
614 const float ay = gravityY_ + forceY_;
615 const float az = gravityZ_ + forceZ_;
616 const float damp = 1.f - damping_;
617 const bool hasInteract = interactRadius_ > 0.f && interactStrength_ != 0.f;
618
619 std::vector<float> aerodynamicAcceleration;
620 if (airDensity_ > 0.f && (dragCoefficient_ > 0.f || liftCoefficient_ > 0.f)) {
621 aerodynamicAcceleration.assign(particles_.size() * 3, 0.f);
622 accumulateAerodynamicAcceleration(dt, aerodynamicAcceleration);
623 }
624
625 for (size_t particleIndex = 0; particleIndex < particles_.size(); ++particleIndex) {
626 Particle& p = particles_[particleIndex];
627 if (p.pinned) {
628 p.px = p.x;
629 p.py = p.y;
630 p.pz = p.z;
631 continue;
632 }
633 const float vx = (p.x - p.px) * damp;
634 const float vy = (p.y - p.py) * damp;
635 const float vz = (p.z - p.pz) * damp;
636 p.px = p.x;
637 p.py = p.y;
638 p.pz = p.z;
639 const float aeroX = aerodynamicAcceleration.empty() ? 0.f : aerodynamicAcceleration[particleIndex * 3];
640 const float aeroY = aerodynamicAcceleration.empty() ? 0.f : aerodynamicAcceleration[particleIndex * 3 + 1];
641 const float aeroZ = aerodynamicAcceleration.empty() ? 0.f : aerodynamicAcceleration[particleIndex * 3 + 2];
642 p.x += vx + (ax + aeroX) * dt * dt;
643 p.y += vy + (ay + aeroY) * dt * dt;
644 p.z += vz + (az + aeroZ) * dt * dt;
645 if (hasInteract) {
646 const float dx = interactX_ - p.x;
647 const float dy = interactY_ - p.y;
648 const float dz = interactZ_ - p.z;
649 const float r2 = dx * dx + dy * dy + dz * dz;
650 const float R2 = interactRadius_ * interactRadius_;
651 if (r2 < R2 && r2 > 1e-6f) {
652 const float r = std::sqrt(r2);
653 const float w = 1.f - r / interactRadius_;
654 const float a = interactStrength_ * w * dt * dt;
655 p.x += (dx / r) * a;
656 p.y += (dy / r) * a;
657 p.z += (dz / r) * a;
658 }
659 }
660 constexpr float maxSpeed = 9.f; // m/s
661 const float maxDisp = maxSpeed * dt;
662 const float dvx = p.x - p.px;
663 const float dvy = p.y - p.py;
664 const float dvz = p.z - p.pz;
665 const float v2 = dvx * dvx + dvy * dvy + dvz * dvz;
666 if (v2 > maxDisp * maxDisp) {
667 const float s = maxDisp / std::sqrt(v2);
668 p.px = p.x - dvx * s;
669 p.py = p.y - dvy * s;
670 p.pz = p.z - dvz * s;
671 }
672 }
673}
674
675void Cloth3D::accumulateAerodynamicAcceleration(float dt, std::vector<float>& accelerations) const {
676 const float invDt = dt > 1e-6f ? 1.f / dt : 0.f;
677 const float invMass = 1.f / particleMass_;
678 for (const Tri& tri : triangles_) {
679 const Particle& a = particles_[static_cast<size_t>(tri.v[0])];
680 const Particle& b = particles_[static_cast<size_t>(tri.v[1])];
681 const Particle& c = particles_[static_cast<size_t>(tri.v[2])];
682
683 const V3 ab{b.x - a.x, b.y - a.y, b.z - a.z};
684 const V3 ac{c.x - a.x, c.y - a.y, c.z - a.z};
685 const V3 areaNormal = cross(ab, ac);
686 const float twiceArea = vlen(areaNormal);
687 if (twiceArea < 1e-8f) continue;
688 V3 normal = mul(1.f / twiceArea, areaNormal);
689
690 const V3 clothVelocity{((a.x - a.px) + (b.x - b.px) + (c.x - c.px)) * (invDt / 3.f),
691 ((a.y - a.py) + (b.y - b.py) + (c.y - c.py)) * (invDt / 3.f),
692 ((a.z - a.pz) + (b.z - b.pz) + (c.z - c.pz)) * (invDt / 3.f)};
693 const V3 relativeWind{windVelocityX_ - clothVelocity.x, windVelocityY_ - clothVelocity.y,
694 windVelocityZ_ - clothVelocity.z};
695 const float speed = vlen(relativeWind);
696 if (speed < 1e-5f) continue;
697 const V3 windDirection = mul(1.f / speed, relativeWind);
698 float incidence = dot(normal, windDirection);
699 if (incidence < 0.f) {
700 normal = mul(-1.f, normal);
701 incidence = -incidence;
702 }
703 if (incidence < 1e-5f) continue;
704
705 const float dynamicPressureArea = 0.5f * airDensity_ * speed * speed * (twiceArea * 0.5f);
706 const V3 drag = mul(dragCoefficient_ * incidence * dynamicPressureArea, windDirection);
707 V3 liftDirection = sub(normal, mul(incidence, windDirection));
708 const float liftLength = vlen(liftDirection);
709 if (liftLength > 1e-6f)
710 liftDirection = mul(1.f / liftLength, liftDirection);
711 else
712 liftDirection = V3{0.f, 0.f, 0.f};
713 const V3 lift = mul(liftCoefficient_ * incidence * dynamicPressureArea, liftDirection);
714 const V3 acceleration = mul(invMass / 3.f, add(drag, lift));
715
716 for (int vertex : tri.v) {
717 if (particles_[static_cast<size_t>(vertex)].pinned) continue;
718 accelerations[static_cast<size_t>(vertex) * 3] += acceleration.x;
719 accelerations[static_cast<size_t>(vertex) * 3 + 1] += acceleration.y;
720 accelerations[static_cast<size_t>(vertex) * 3 + 2] += acceleration.z;
721 }
722 }
723}
724
725void Cloth3D::solveConstraints(float dt) {
726 for (Link& link : links_) link.lambda = 0.f;
727 for (Tether& tether : tethers_) tether.lambda = 0.f;
728 for (SkinConstraint& skin : skinConstraints_) skin.lambda = 0.f;
729 for (Attachment& attachment : attachments_) attachment.lambda = 0.f;
730 volumeLambda_ = 0.f;
731 const float invDtSquared = dt > 1e-8f ? 1.f / (dt * dt) : 0.f;
732 for (int iter = 0; iter < iterations_; ++iter) {
733 for (Link& link : links_) {
734 Particle& a = particles_[static_cast<size_t>(link.a)];
735 Particle& b = particles_[static_cast<size_t>(link.b)];
736 if (a.pinned && b.pinned) continue;
737 float dx = b.x - a.x;
738 float dy = b.y - a.y;
739 float dz = b.z - a.z;
740 const float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
741 if (dist < 1e-5f) continue;
742 float compliance = stretchCompliance_;
743 if (link.kind == ClothConstraintKind::Shear)
744 compliance = shearCompliance_;
745 else if (link.kind == ClothConstraintKind::Bend)
746 compliance = bendCompliance_;
747 if (compliance > 0.f) {
748 const float weightA = a.pinned ? 0.f : a.inverseMass;
749 const float weightB = b.pinned ? 0.f : b.inverseMass;
750 const float alpha = compliance * invDtSquared;
751 const float deltaLambda = (-(dist - link.rest) - alpha * link.lambda) / (weightA + weightB + alpha);
752 link.lambda += deltaLambda;
753 const float nx = dx / dist;
754 const float ny = dy / dist;
755 const float nz = dz / dist;
756 a.x -= weightA * nx * deltaLambda;
757 a.y -= weightA * ny * deltaLambda;
758 a.z -= weightA * nz * deltaLambda;
759 b.x += weightB * nx * deltaLambda;
760 b.y += weightB * ny * deltaLambda;
761 b.z += weightB * nz * deltaLambda;
762 continue;
763 }
764 const float diff = (dist - link.rest) / dist * stiffness_;
765 if (a.pinned) {
766 b.x -= dx * diff;
767 b.y -= dy * diff;
768 b.z -= dz * diff;
769 } else if (b.pinned) {
770 a.x += dx * diff;
771 a.y += dy * diff;
772 a.z += dz * diff;
773 } else {
774 const float half = diff * 0.5f;
775 a.x += dx * half;
776 a.y += dy * half;
777 a.z += dz * half;
778 b.x -= dx * half;
779 b.y -= dy * half;
780 b.z -= dz * half;
781 }
782 }
783 solveTetherConstraints(dt);
784 solveVolumeConstraint(dt);
785 solveSkinConstraints(dt);
786 solveAttachments(dt);
787 if (validIndex(grabIndex_)) {
788 Particle& g = particles_[static_cast<size_t>(grabIndex_)];
789 g.x = grabX_;
790 g.y = grabY_;
791 g.z = grabZ_;
792 g.px = grabX_;
793 g.py = grabY_;
794 g.pz = grabZ_;
795 }
796 }
797}
798
799void Cloth3D::solveFoldConstraint() {
800 if (foldStiffness_ <= 0.f || maxFoldAngle_ >= kPi || foldPairs_.empty()) return;
801 // Fold pairs share edge (a,b) with opposite vertices k (tri 1) and l (tri 2).
802 // For a flat sheet the in-plane directions m1,m2 point to opposite sides
803 // (theta = pi); folding brings them toward the same side (theta -> 0). When
804 // theta drops below pi - maxFoldAngle, rotate both triangles rigidly around
805 // the shared edge to reopen the fold to the limit.
806 const float thetaMin = kPi - maxFoldAngle_;
807 const float s = foldStiffness_;
808 for (const FoldPair& pair : foldPairs_) {
809 Particle& pa = particles_[static_cast<size_t>(pair.a)];
810 Particle& pb = particles_[static_cast<size_t>(pair.b)];
811 Particle& pk = particles_[static_cast<size_t>(pair.k)];
812 Particle& pl = particles_[static_cast<size_t>(pair.l)];
813 if (pa.pinned && pb.pinned && pk.pinned && pl.pinned) continue;
814
815 const V3 paV{pa.x, pa.y, pa.z};
816 const V3 pbV{pb.x, pb.y, pb.z};
817 V3 pkV{pk.x, pk.y, pk.z};
818 V3 plV{pl.x, pl.y, pl.z};
819
820 const V3 u = vnorm(sub(pbV, paV));
821 const V3 vk = sub(pkV, paV);
822 const V3 vl = sub(plV, paV);
823 const V3 h1 = sub(vk, mul(dot(vk, u), u));
824 const V3 h2 = sub(vl, mul(dot(vl, u), u));
825 const float b1 = vlen(h1);
826 const float b2 = vlen(h2);
827 if (b1 < 1e-6f || b2 < 1e-6f) continue;
828 const V3 m1 = mul(1.f / b1, h1);
829 const V3 m2 = mul(1.f / b2, h2);
830 const float theta = std::acos(std::clamp(dot(m1, m2), -1.f, 1.f));
831 if (theta >= thetaMin) continue;
832
833 // Reopen the fold along its own path: a pure fold moves m1/m2 from
834 // opposite (flat, theta = pi) toward the same side (theta -> 0) with a
835 // fixed rotation sign; rotate both triangles back by delta/2 each.
836 const float phi = signedAngle(m1, m2, u);
837 const float sign = phi >= 0.f ? 1.f : -1.f;
838 const float delta = thetaMin - theta;
839 const float a1 = -sign * delta * 0.5f * s;
840 const float a2 = sign * delta * 0.5f * s;
841 if (!pk.pinned) {
842 pkV = add(paV, rotateAround(vk, u, a1 * s));
843 pk.x = pkV.x;
844 pk.y = pkV.y;
845 pk.z = pkV.z;
846 }
847 if (!pl.pinned) {
848 plV = add(paV, rotateAround(vl, u, a2 * s));
849 pl.x = plV.x;
850 pl.y = plV.y;
851 pl.z = plV.z;
852 }
853 }
854}
855
856void Cloth3D::solveSelfCollision() {
857 if (!selfCollision_ || getParticleCount() == 0) return;
858 const float minDist = particleSize_ * 2.f;
859 if (minDist <= 0.f) return;
860 rebuildHash();
861 const float cell = std::max(1e-3f, minDist);
862 const float inv = 1.f / cell;
863 for (int i = 0; i < getParticleCount(); ++i) {
864 Particle& pi = particles_[static_cast<size_t>(i)];
865 const int cx = int(std::floor(pi.x * inv));
866 const int cy = int(std::floor(pi.y * inv));
867 const int cz = int(std::floor(pi.z * inv));
868 for (int oz = -1; oz <= 1; ++oz) {
869 for (int oy = -1; oy <= 1; ++oy) {
870 for (int ox = -1; ox <= 1; ++ox) {
871 auto it = hash_.find(cellKey(cx + ox, cy + oy, cz + oz));
872 if (it == hash_.end()) continue;
873 for (int j : it->second) {
874 if (j <= i) continue;
875 if (areLinked(i, j)) continue;
876 Particle& pj = particles_[static_cast<size_t>(j)];
877 float dx = pj.x - pi.x;
878 float dy = pj.y - pi.y;
879 float dz = pj.z - pi.z;
880 const float d2 = dx * dx + dy * dy + dz * dz;
881 if (d2 >= minDist * minDist || d2 < 1e-8f) continue;
882 if (pi.pinned && pj.pinned) continue;
883 const float d = std::sqrt(d2);
884 const float corr = std::min(0.5f, (minDist - d) / d);
885 float wa = 0.5f;
886 float wb = 0.5f;
887 if (pi.pinned && !pj.pinned) {
888 wa = 0.f;
889 wb = 1.f;
890 } else if (pj.pinned && !pi.pinned) {
891 wa = 1.f;
892 wb = 0.f;
893 }
894 pi.x -= dx * corr * wa;
895 pi.y -= dy * corr * wa;
896 pi.z -= dz * corr * wa;
897 pj.x += dx * corr * wb;
898 pj.y += dy * corr * wb;
899 pj.z += dz * corr * wb;
900 }
901 }
902 }
903 }
904 }
905}
906
907void Cloth3D::solveSelfCollisionTriangles() {
908 if (!selfCollision_ || triangles_.size() < 2) return;
909 const float thickness = std::max(1e-3f, particleSize_ * 2.f);
910 const int triCount = static_cast<int>(triangles_.size());
911
912 std::vector<V3> pos(particles_.size());
913 for (size_t i = 0; i < particles_.size(); ++i) {
914 pos[i] = {particles_[i].x, particles_[i].y, particles_[i].z};
915 }
916
917 const float cell = thickness;
918 const float inv = 1.f / cell;
919 std::unordered_map<int64_t, std::vector<int>> triHash;
920 auto triCellKey = [](int cx, int cy, int cz) -> int64_t {
921 return int64_t(uint32_t(cx) & 0x1fffffu) | (int64_t(uint32_t(cy) & 0x1fffffu) << 21) |
922 (int64_t(uint32_t(cz) & 0x1fffffu) << 42);
923 };
924 const auto insertTri = [&](int ti) {
925 const Tri& tri = triangles_[static_cast<size_t>(ti)];
926 V3 mn = pos[static_cast<size_t>(tri.v[0])];
927 V3 mx = mn;
928 for (int k = 1; k < 3; ++k) {
929 const V3& p = pos[static_cast<size_t>(tri.v[k])];
930 mn.x = std::min(mn.x, p.x);
931 mn.y = std::min(mn.y, p.y);
932 mn.z = std::min(mn.z, p.z);
933 mx.x = std::max(mx.x, p.x);
934 mx.y = std::max(mx.y, p.y);
935 mx.z = std::max(mx.z, p.z);
936 }
937 const V3 pad{thickness, thickness, thickness};
938 mn = sub(mn, pad);
939 mx = add(mx, pad);
940 const int x0 = int(std::floor(mn.x * inv));
941 const int y0 = int(std::floor(mn.y * inv));
942 const int z0 = int(std::floor(mn.z * inv));
943 const int x1 = int(std::floor(mx.x * inv));
944 const int y1 = int(std::floor(mx.y * inv));
945 const int z1 = int(std::floor(mx.z * inv));
946 for (int cz = z0; cz <= z1; ++cz)
947 for (int cy = y0; cy <= y1; ++cy)
948 for (int cx = x0; cx <= x1; ++cx) triHash[triCellKey(cx, cy, cz)].push_back(ti);
949 };
950 for (int ti = 0; ti < triCount; ++ti) insertTri(ti);
951
952 const auto sharesVertex = [&](const Tri& a, const Tri& b) {
953 for (int i = 0; i < 3; ++i)
954 for (int j = 0; j < 3; ++j)
955 if (a.v[i] == b.v[j]) return true;
956 return false;
957 };
958
959 std::unordered_set<int64_t> processed;
960 std::vector<int> cells;
961 int featA[3];
962 int featB[3];
963
964 for (int i = 0; i < triCount; ++i) {
965 const Tri& triI = triangles_[static_cast<size_t>(i)];
966 const V3& pa0 = pos[static_cast<size_t>(triI.v[0])];
967 V3 mn = pa0;
968 V3 mx = mn;
969 for (int k = 1; k < 3; ++k) {
970 const V3& p = pos[static_cast<size_t>(triI.v[k])];
971 mn.x = std::min(mn.x, p.x);
972 mn.y = std::min(mn.y, p.y);
973 mn.z = std::min(mn.z, p.z);
974 mx.x = std::max(mx.x, p.x);
975 mx.y = std::max(mx.y, p.y);
976 mx.z = std::max(mx.z, p.z);
977 }
978 const V3 pad{thickness, thickness, thickness};
979 mn = sub(mn, pad);
980 mx = add(mx, pad);
981 const int x0 = int(std::floor(mn.x * inv));
982 const int y0 = int(std::floor(mn.y * inv));
983 const int z0 = int(std::floor(mn.z * inv));
984 const int x1 = int(std::floor(mx.x * inv));
985 const int y1 = int(std::floor(mx.y * inv));
986 const int z1 = int(std::floor(mx.z * inv));
987 cells.clear();
988 for (int cz = z0; cz <= z1; ++cz)
989 for (int cy = y0; cy <= y1; ++cy)
990 for (int cx = x0; cx <= x1; ++cx) {
991 auto it = triHash.find(triCellKey(cx, cy, cz));
992 if (it != triHash.end()) cells.insert(cells.end(), it->second.begin(), it->second.end());
993 }
994 for (int j : cells) {
995 if (j <= i) continue;
996 const int64_t key = pairKey(i, j);
997 if (!processed.insert(key).second) continue;
998 const Tri& triJ = triangles_[static_cast<size_t>(j)];
999 if (sharesVertex(triI, triJ)) continue;
1000
1001 const V3 A[3] = {pos[static_cast<size_t>(triI.v[0])], pos[static_cast<size_t>(triI.v[1])],
1002 pos[static_cast<size_t>(triI.v[2])]};
1003 const V3 B[3] = {pos[static_cast<size_t>(triJ.v[0])], pos[static_cast<size_t>(triJ.v[1])],
1004 pos[static_cast<size_t>(triJ.v[2])]};
1005 V3 cpA;
1006 V3 cpB;
1007 int countA = 0;
1008 int countB = 0;
1009 const float d = triangleDistance(A, B, featA, featB, countA, countB, cpA, cpB);
1010 if (d >= thickness) continue;
1011
1012 V3 n = vnorm(sub(cpB, cpA));
1013 if (vlen(sub(cpB, cpA)) < 1e-6f) {
1014 n = vnorm(cross(sub(B[1], B[0]), sub(B[2], B[0])));
1015 }
1016 const float corr = (thickness - d) * stiffness_;
1017 if (corr <= 0.f) continue;
1018
1019 const auto freeCount = [&](const int* feat, int count) {
1020 int free = 0;
1021 for (int k = 0; k < count; ++k)
1022 if (!particles_[static_cast<size_t>(feat[k])].pinned) ++free;
1023 return free;
1024 };
1025 const int freeA = freeCount(featA, countA);
1026 const int freeB = freeCount(featB, countB);
1027 if (freeA == 0 && freeB == 0) continue;
1028 const float wA = freeA > 0 ? (freeB > 0 ? 0.5f : 1.f) : 0.f;
1029 const float wB = freeB > 0 ? (freeA > 0 ? 0.5f : 1.f) : 0.f;
1030 const auto pushFeature = [&](const int* feat, int count, const V3& dir) {
1031 for (int k = 0; k < count; ++k) {
1032 Particle& pv = particles_[static_cast<size_t>(feat[k])];
1033 if (pv.pinned) continue;
1034 pv.x += dir.x;
1035 pv.y += dir.y;
1036 pv.z += dir.z;
1037 }
1038 };
1039 if (freeA > 0) {
1040 const V3 dir = mul(-corr * wA / float(freeA), n);
1041 pushFeature(featA, countA, dir);
1042 }
1043 if (freeB > 0) {
1044 const V3 dir = mul(corr * wB / float(freeB), n);
1045 pushFeature(featB, countB, dir);
1046 }
1047 }
1048 }
1049
1050 // Vertex-through-face: push any vertex that has crossed into the prism of a
1051 // non-adjacent triangle back out along the face normal.
1052 for (int i = 0; i < triCount; ++i) {
1053 const Tri& triI = triangles_[static_cast<size_t>(i)];
1054 const V3 A[3] = {pos[static_cast<size_t>(triI.v[0])], pos[static_cast<size_t>(triI.v[1])],
1055 pos[static_cast<size_t>(triI.v[2])]};
1056 const V3 nI = vnorm(cross(sub(A[1], A[0]), sub(A[2], A[0])));
1057 for (int j = i + 1; j < triCount; ++j) {
1058 const Tri& triJ = triangles_[static_cast<size_t>(j)];
1059 if (sharesVertex(triI, triJ)) continue;
1060 const V3 B[3] = {pos[static_cast<size_t>(triJ.v[0])], pos[static_cast<size_t>(triJ.v[1])],
1061 pos[static_cast<size_t>(triJ.v[2])]};
1062 const V3 nJ = vnorm(cross(sub(B[1], B[0]), sub(B[2], B[0])));
1063 // Vertices of tri i against the face of tri j.
1064 for (int k = 0; k < 3; ++k) {
1065 Particle& pv = particles_[static_cast<size_t>(triI.v[k])];
1066 if (pv.pinned) continue;
1067 const V3 va = A[k];
1068 const float d = dot(sub(va, B[0]), nJ);
1069 if (std::fabs(d) >= thickness) continue;
1070 const V3 q = add(va, mul(-d, nJ));
1071 if (!pointInTriangle(q, B[0], B[1], B[2])) continue;
1072 const float push = (thickness - std::fabs(d)) * stiffness_ * 0.5f;
1073 const V3 dir = mul(d >= 0.f ? push : -push, nJ);
1074 pv.x += dir.x;
1075 pv.y += dir.y;
1076 pv.z += dir.z;
1077 }
1078 // Vertices of tri j against the face of tri i.
1079 for (int k = 0; k < 3; ++k) {
1080 Particle& pv = particles_[static_cast<size_t>(triJ.v[k])];
1081 if (pv.pinned) continue;
1082 const V3 vb = B[k];
1083 const float d = dot(sub(vb, A[0]), nI);
1084 if (std::fabs(d) >= thickness) continue;
1085 const V3 q = add(vb, mul(-d, nI));
1086 if (!pointInTriangle(q, A[0], A[1], A[2])) continue;
1087 const float push = (thickness - std::fabs(d)) * stiffness_ * 0.5f;
1088 const V3 dir = mul(d >= 0.f ? push : -push, nI);
1089 pv.x += dir.x;
1090 pv.y += dir.y;
1091 pv.z += dir.z;
1092 }
1093 }
1094 }
1095}
1096
1097void Cloth3D::collideWorld(float dt) {
1098 if (!world_ || !world_->isValid() || particleSize_ <= 0.f) return;
1099 const float invDt = dt > 1e-6f ? 1.f / dt : 0.f;
1100 ClothContact3D contact;
1101 for (int i = 0; i < getParticleCount(); ++i) {
1102 Particle& p = particles_[static_cast<size_t>(i)];
1103 if (p.pinned) continue;
1104 if (world_->pointProbeFiltered(p.x, p.y, p.z, particleSize_, &contact, collisionCategoryBits_,
1105 collisionMaskBits_) != ClothProbeStatus::Hit ||
1106 !contact.hit)
1107 continue;
1108 const float vpx = (p.x - p.px) * invDt;
1109 const float vpy = (p.y - p.py) * invDt;
1110 const float vpz = (p.z - p.pz) * invDt;
1111 p.x += contact.nx * contact.depth;
1112 p.y += contact.ny * contact.depth;
1113 p.z += contact.nz * contact.depth;
1114
1115 float vbx = 0.f;
1116 float vby = 0.f;
1117 float vbz = 0.f;
1118 float bodyMass = 0.f;
1119 const bool dynamic = contact.body != nullptr && contact.body->getType() == "dynamic";
1120 if (dynamic) {
1121 vbx = contact.body->getLinearVelocityX();
1122 vby = contact.body->getLinearVelocityY();
1123 vbz = contact.body->getLinearVelocityZ();
1124 bodyMass = contact.body->getMass();
1125 }
1126 const float vn = (vpx - vbx) * contact.nx + (vpy - vby) * contact.ny + (vpz - vbz) * contact.nz;
1127 if (vn < 0.f) {
1128 const float restitution = std::max(collisionRestitution_, contact.restitution);
1129 const float friction = std::clamp(std::sqrt(collisionFriction_ * std::max(0.f, contact.friction)), 0.f, 1.f);
1130 const float relativeX = vpx - vbx;
1131 const float relativeY = vpy - vby;
1132 const float relativeZ = vpz - vbz;
1133 const float tangentX = relativeX - vn * contact.nx;
1134 const float tangentY = relativeY - vn * contact.ny;
1135 const float tangentZ = relativeZ - vn * contact.nz;
1136 const float nextNormal = -vn * restitution;
1137 const float nextVx = vbx + tangentX * (1.f - friction) + contact.nx * nextNormal;
1138 const float nextVy = vby + tangentY * (1.f - friction) + contact.ny * nextNormal;
1139 const float nextVz = vbz + tangentZ * (1.f - friction) + contact.nz * nextNormal;
1140 p.px = p.x - nextVx * dt;
1141 p.py = p.y - nextVy * dt;
1142 p.pz = p.z - nextVz * dt;
1143 const float m = particleMass_;
1144 const float reduced = bodyMass > 0.f ? (m * bodyMass) / (m + bodyMass) : m;
1145 float j = -(1.f + restitution) * vn * reduced;
1146 const float maxKick = 9.f; // m/s
1147 j = std::min(j, maxKick * m);
1148 const float kick = (j / m) * dt;
1149 p.x += contact.nx * kick;
1150 p.y += contact.ny * kick;
1151 p.z += contact.nz * kick;
1152 if (dynamic && bodyMass > 0.f) {
1153 contact.body->applyLinearImpulse(-contact.nx * j, -contact.ny * j, -contact.nz * j);
1154 }
1155 }
1156 }
1157}
1158
1159void Cloth3D::collideBounds() {
1160 if (!hasBounds_) return;
1161 const float minX = boundX_;
1162 const float minY = boundY_;
1163 const float minZ = boundZ_;
1164 const float maxX = boundX_ + boundW_;
1165 const float maxY = boundY_ + boundH_;
1166 const float maxZ = boundZ_ + boundD_;
1167 const float bounce = collisionRestitution_;
1168
1169 for (Particle& p : particles_) {
1170 if (p.pinned) continue;
1171 if (p.x < minX) {
1172 const float vx = p.x - p.px;
1173 p.x = minX;
1174 p.px = p.x + vx * bounce;
1175 } else if (p.x > maxX) {
1176 const float vx = p.x - p.px;
1177 p.x = maxX;
1178 p.px = p.x + vx * bounce;
1179 }
1180 if (p.y < minY) {
1181 const float vy = p.y - p.py;
1182 p.y = minY;
1183 p.py = p.y + vy * bounce;
1184 } else if (p.y > maxY) {
1185 const float vy = p.y - p.py;
1186 p.y = maxY;
1187 p.py = p.y + vy * bounce;
1188 }
1189 if (p.z < minZ) {
1190 const float vz = p.z - p.pz;
1191 p.z = minZ;
1192 p.pz = p.z + vz * bounce;
1193 } else if (p.z > maxZ) {
1194 const float vz = p.z - p.pz;
1195 p.z = maxZ;
1196 p.pz = p.z + vz * bounce;
1197 }
1198 }
1199}
1200
1201void Cloth3D::update(float dt) {
1202 if (destroyed_) return;
1203 if (dt < 0.f) dt = 0.f;
1204 if (dt > 0.05f) dt = 0.05f;
1205
1206 updateSubsteps(dt, 2);
1207}
1208
1209void Cloth3D::updateSubsteps(float dt, int substeps) {
1210 if (destroyed_ || substeps < 1) return;
1211
1212 const float h = dt / float(substeps);
1213 for (int s = 0; s < substeps; ++s) {
1214 integrate(h);
1215 applyAutomaticTearing();
1216 solveConstraints(h);
1217 solveFoldConstraint();
1218 solveSelfCollision();
1219 solveSelfCollisionTriangles();
1220 collideWorld(h);
1221 collideBounds();
1222 if (grabIndex_ >= 0) {
1223 Particle& p = particles_[static_cast<size_t>(grabIndex_)];
1224 p.px = p.x;
1225 p.py = p.y;
1226 p.pz = p.z;
1227 }
1228 }
1229 forceX_ = 0.f;
1230 forceY_ = 0.f;
1231 forceZ_ = 0.f;
1232 interactStrength_ = 0.f;
1233}
1234
1236 if (destroyed_)
1238 eve::DiagnosticCode::PreconditionViolation, "Cannot step a destroyed cloth", "physics.cloth3d.step"));
1239 auto valid = detail::validateSimulationStep(stepValue, settings, observation_);
1240 if (!valid) return valid;
1241 auto next = detail::advanceSimulationObservation(observation_, stepValue);
1242 if (!next) return eve::Result<void>::failure(next.status());
1243 try {
1244 updateSubsteps(static_cast<float>(stepValue.delta.seconds()), settings.subStepCount);
1245 } catch (const std::exception& error) {
1247 eve::DiagnosticCode::Failed, std::string("Cloth3D step failed: ") + error.what(), "physics.cloth3d.step"));
1248 } catch (...) {
1250 eve::DiagnosticCode::Failed, "Cloth3D step failed with an unknown exception", "physics.cloth3d.step"));
1251 }
1252 observation_ = std::move(next).takeValue();
1254}
1255
1257 auto valid = detail::validateSimulationObservation(observation, "physics.cloth3d.restoreObservation");
1258 if (!valid) return valid;
1259 if (destroyed_)
1261 "Cannot restore a destroyed cloth",
1262 "physics.cloth3d.restoreObservation"));
1263 observation_ = observation;
1265}
1266
1268 if (!gfx || destroyed_ || getParticleCount() < 4) return;
1269
1270 // Double-sided mesh: front vertices carry the face normals, back vertices
1271 // duplicate positions with flipped normals and reversed winding.
1272 const int gridVertices = cols_ * rows_;
1273 const int vertexCount = gridVertices * 2;
1274 const int quadCount = (cols_ - 1) * (rows_ - 1);
1275 const int indexCount = quadCount * 12;
1276
1277 std::vector<float> pos(static_cast<size_t>(vertexCount) * 3);
1278 std::vector<float> nrm(static_cast<size_t>(vertexCount) * 3, 0.f);
1279 std::vector<float> uv(static_cast<size_t>(vertexCount) * 2);
1280 for (int r = 0; r < rows_; ++r) {
1281 for (int c = 0; c < cols_; ++c) {
1282 const size_t i = static_cast<size_t>(r * cols_ + c);
1283 const Particle& p = particles_[i];
1284 const float u = cols_ > 1 ? float(c) / float(cols_ - 1) : 0.f;
1285 const float v = rows_ > 1 ? float(r) / float(rows_ - 1) : 0.f;
1286 for (int side = 0; side < 2; ++side) {
1287 const size_t vi = i + static_cast<size_t>(side) * gridVertices;
1288 pos[vi * 3 + 0] = p.x;
1289 pos[vi * 3 + 1] = p.y;
1290 pos[vi * 3 + 2] = p.z;
1291 uv[vi * 2 + 0] = u;
1292 uv[vi * 2 + 1] = v;
1293 }
1294 }
1295 }
1296
1297 // Accumulate face normals.
1298 for (const Tri& tri : triangles_) {
1299 const float* pa = &pos[static_cast<size_t>(tri.v[0]) * 3];
1300 const float* pb = &pos[static_cast<size_t>(tri.v[1]) * 3];
1301 const float* pc = &pos[static_cast<size_t>(tri.v[2]) * 3];
1302 float e1x = pb[0] - pa[0], e1y = pb[1] - pa[1], e1z = pb[2] - pa[2];
1303 float e2x = pc[0] - pa[0], e2y = pc[1] - pa[1], e2z = pc[2] - pa[2];
1304 float nx = e1y * e2z - e1z * e2y;
1305 float ny = e1z * e2x - e1x * e2z;
1306 float nz = e1x * e2y - e1y * e2x;
1307 const float len = std::sqrt(nx * nx + ny * ny + nz * nz);
1308 if (len > 1e-8f) {
1309 nx /= len;
1310 ny /= len;
1311 nz /= len;
1312 } else {
1313 nx = 0.f;
1314 ny = 1.f;
1315 nz = 0.f;
1316 }
1317 for (int v : tri.v) {
1318 nrm[static_cast<size_t>(v) * 3 + 0] += nx;
1319 nrm[static_cast<size_t>(v) * 3 + 1] += ny;
1320 nrm[static_cast<size_t>(v) * 3 + 2] += nz;
1321 nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 0] -= nx;
1322 nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 1] -= ny;
1323 nrm[(static_cast<size_t>(v) + gridVertices) * 3 + 2] -= nz;
1324 }
1325 }
1326 for (int i = 0; i < gridVertices; ++i) {
1327 const float nx = nrm[static_cast<size_t>(i) * 3 + 0];
1328 const float ny = nrm[static_cast<size_t>(i) * 3 + 1];
1329 const float nz = nrm[static_cast<size_t>(i) * 3 + 2];
1330 const float len = std::sqrt(nx * nx + ny * ny + nz * nz);
1331 for (int side = 0; side < 2; ++side) {
1332 const size_t vi = static_cast<size_t>(i) + static_cast<size_t>(side) * gridVertices;
1333 const float sign = side == 0 ? 1.f : -1.f;
1334 if (len > 1e-8f) {
1335 nrm[vi * 3 + 0] = sign * nx / len;
1336 nrm[vi * 3 + 1] = sign * ny / len;
1337 nrm[vi * 3 + 2] = sign * nz / len;
1338 } else {
1339 nrm[vi * 3 + 1] = sign;
1340 }
1341 }
1342 }
1343
1344 if (!mesh_ || meshVertexCount_ != vertexCount || meshIndexCount_ != indexCount) {
1345 std::vector<uint32_t> indices(static_cast<size_t>(indexCount));
1346 int out = 0;
1347 for (int r = 0; r + 1 < rows_; ++r) {
1348 for (int c = 0; c + 1 < cols_; ++c) {
1349 const uint32_t a = static_cast<uint32_t>(r * cols_ + c);
1350 const uint32_t b = static_cast<uint32_t>(r * cols_ + c + 1);
1351 const uint32_t cc = static_cast<uint32_t>((r + 1) * cols_ + c + 1);
1352 const uint32_t d = static_cast<uint32_t>((r + 1) * cols_ + c);
1353 // Front faces (CCW when viewed from +Y).
1354 indices[static_cast<size_t>(out) + 0] = a;
1355 indices[static_cast<size_t>(out) + 1] = d;
1356 indices[static_cast<size_t>(out) + 2] = b;
1357 indices[static_cast<size_t>(out) + 3] = d;
1358 indices[static_cast<size_t>(out) + 4] = cc;
1359 indices[static_cast<size_t>(out) + 5] = b;
1360 // Back faces (reversed winding on the duplicated vertex set).
1361 const uint32_t oa = a + static_cast<uint32_t>(gridVertices);
1362 const uint32_t ob = b + static_cast<uint32_t>(gridVertices);
1363 const uint32_t occ = cc + static_cast<uint32_t>(gridVertices);
1364 const uint32_t od = d + static_cast<uint32_t>(gridVertices);
1365 indices[static_cast<size_t>(out) + 6] = ob;
1366 indices[static_cast<size_t>(out) + 7] = od;
1367 indices[static_cast<size_t>(out) + 8] = oa;
1368 indices[static_cast<size_t>(out) + 9] = ob;
1369 indices[static_cast<size_t>(out) + 10] = occ;
1370 indices[static_cast<size_t>(out) + 11] = od;
1371 out += 12;
1372 }
1373 }
1374 mesh_ = gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), vertexCount, indices.data(), indexCount);
1375 meshVertexCount_ = vertexCount;
1376 meshIndexCount_ = indexCount;
1377 } else {
1378 gfx->updateMeshVertices(mesh_, pos.data(), nrm.data(), uv.data(), vertexCount, nullptr, 0);
1379 }
1380
1381 if (mesh_) {
1382 gfx->drawMesh(mesh_, glm::mat4(1.f), nullptr, Color(colorR_, colorG_, colorB_, colorA_));
1383 }
1384}
1385
1386} // namespace eve::physics
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
const std::string & s
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float degrees
Definition CardTypes.cpp:35
float uv
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int az
Definition CaveMesh.cpp:113
float thickness
Definition CaveMesh.cpp:83
float nx
float nz
float ny
glm::vec4 p[6]
int rows
int cols
std::uint32_t vertexCount
std::uint32_t indexCount
tensor::Graph g
Definition GpuGraph.cpp:7
std::uint32_t key
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
std::uint32_t ab
double r
std::uint32_t ac
std::vector< std::uint32_t > indices
HexDirection windDirection
float v
std::int32_t second
std::int32_t c
eve::ResourcePin pinned
int h
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< TriangleRef > triangles
Texture * normal
std::string error
Definition Package.cpp:60
int idx
float f
World3D * world
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
float d
float t
glm::mat4 model
float dz
float dy
float dx
std::uint32_t count
double restitution
Cell cell
std::map< Cell, int > best
ecs::EntityHandle side
TerrainThermalSettings settings
int spacing
int iterations
Definition TreeMesh.cpp:311
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
uint32_t index
const UnitySourceAsset & source
std::vector< double > phi
double oy
double ox
std::vector< int > edges
float m[16]
float vz
float vy
float vx
float angle
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
Definition Time.cpp:28
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
virtual void drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint)=0
Draw one mesh with model matrix. Requires begin3DFrame() (or an open swapchain pass).
virtual bool updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
In-place update of a mesh's vertex/index data (CPU -> host-visible VBO). Mirrors bakeMeshMorph: the u...
virtual Mesh * newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
Upload a triangle mesh from packed CPU arrays. Owned by Graphics. posXYZ required (vertexCount*3)....
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
void clearBounds()
Clears bounds.
Definition Cloth3D.cpp:465
float getWindVelocityZ() const
Return configured air velocity along Z in m/s.
Definition Cloth3D.cpp:431
void setGravity(float gx, float gy, float gz)
Sets the gravity.
Definition Cloth3D.cpp:349
void setCollisionMaterial(float friction, float restitution)
Configure cloth contact friction and restitution in [0,1].
Definition Cloth3D.cpp:467
void pin(int index)
Definition Cloth3D.cpp:477
~Cloth3D()
Cloth 3 d.
Definition Cloth3D.cpp:226
void setParticleInverseMass(int index, float inverseMass)
Set per-particle inverse mass; zero makes the particle kinematic/pinned.
Definition Cloth3D.cpp:500
void interactAt(float x, float y, float z, float radius, float strength)
Pointer-field interaction like Fluid2D::interactAt (3D): positive strength attracts,...
Definition Cloth3D.cpp:552
Cloth3D(int cols, int rows, float spacing, float originX, float originY, float originZ)
Cloth 3 d.
Definition Cloth3D.cpp:216
void destroy()
Destroys .
Definition Cloth3D.cpp:228
void setShearCompliance(float compliance)
Set XPBD shear compliance in m/N; zero preserves legacy PBD.
Definition Cloth3D.cpp:361
float getDragCoefficient() const
Return the aerodynamic drag coefficient.
Definition Cloth3D.cpp:441
void moveGrab(float x, float y, float z)
Moves grab.
Definition Cloth3D.cpp:530
void setCollideWorld(World3D *world)
Attach a World3D so free particles collide with its non-sensor shapes in meter space....
Definition Cloth3D.cpp:560
void draw(graphics::Graphics *gfx)
Draw the cloth as a triangle mesh (requires an open 3D frame).
Definition Cloth3D.cpp:1267
float getLiftCoefficient() const
Return the aerodynamic lift coefficient.
Definition Cloth3D.cpp:443
void setWindVelocity(float vx, float vy, float vz)
Set world-space air velocity used by the aerodynamic model (m/s).
Definition Cloth3D.cpp:421
void setStretchCompliance(float compliance)
Set XPBD stretch compliance in m/N; zero preserves legacy PBD.
Definition Cloth3D.cpp:357
void setSelfCollision(bool on)
Enable proximity-based self-collision between non-adjacent particles and triangles (default true)....
Definition Cloth3D.cpp:445
int grabAt(float x, float y, float z, float radius=0.3f)
Grab nearest free particle within radius (meters). Returns particle index, or -1 if none.
Definition Cloth3D.cpp:511
bool isPinned(int index) const
True when pinned.
Definition Cloth3D.cpp:495
void pinTopRow()
Pin top row.
Definition Cloth3D.cpp:491
void setColor(float r, float g, float b, float a=1.f)
Sets the color.
Definition Cloth3D.cpp:562
void setStiffness(float stiffness)
Constraint relaxation strength in [0,1] (default 0.85).
Definition Cloth3D.cpp:355
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings) override
Advances cloth with the shared ticked backend contract.
Definition Cloth3D.cpp:1235
void setCollisionFilter(uint64_t categoryBits, uint64_t maskBits)
Configure symmetric category/mask filtering for World3D contacts.
Definition Cloth3D.cpp:472
float getParticleX(int index) const
Definition Cloth3D.cpp:569
void setBendCompliance(float compliance)
Set XPBD distance-bend compliance in m/N; zero preserves legacy PBD.
Definition Cloth3D.cpp:365
void unpin(int index)
Unpin.
Definition Cloth3D.cpp:484
eve::Result< void > restoreObservation(const SimulationObservation &observation) override
Restores tick/progress metadata after an owner-level restore.
Definition Cloth3D.cpp:1256
void setBounds(float x, float y, float z, float w, float h, float d)
Axis-aligned box (origin + extents, meters); particles bounce inside.
Definition Cloth3D.cpp:451
void reset()
Restore the flat grid pose (top row pinned) and clear transient state.
Definition Cloth3D.cpp:230
void releaseGrab()
Release grab.
Definition Cloth3D.cpp:544
SimulationObservation observation() const noexcept override
Returns completed tick/time observables.
Definition Cloth3D.h:69
void setParticleSize(float size)
Particle radius in meters (default 0.1): self-collision separation, draw scale and rigid-body collisi...
Definition Cloth3D.cpp:373
void setIterations(int iterations)
Constraint solver iterations per substep (default 4).
Definition Cloth3D.cpp:369
void update(float dt)
Updates .
Definition Cloth3D.cpp:1201
void setDamping(float damping)
Damping applied to Verlet velocity [0,1] (default 0.01).
Definition Cloth3D.cpp:371
float getWindVelocityY() const
Return configured air velocity along Y in m/s.
Definition Cloth3D.cpp:429
float getParticleInverseMass(int index) const
Return per-particle inverse mass, or zero for an invalid index.
Definition Cloth3D.cpp:507
void setParticleMass(float mass)
Implicit particle mass in kg (default 0.1). Used for mass-proportional momentum exchange when collidi...
Definition Cloth3D.cpp:375
void applyForce(float fx, float fy, float fz)
Applies force.
Definition Cloth3D.cpp:546
float getParticleZ(int index) const
Returns the particle z.
Definition Cloth3D.cpp:579
void setParticlePosition(int index, float x, float y, float z)
Sets the particle position.
Definition Cloth3D.cpp:584
void setMaxFoldAngle(float degrees)
Maximum fold angle between adjacent triangles in degrees Range is 0..180 degrees; default is 120....
Definition Cloth3D.cpp:449
int getParticleCount() const
Returns the particle count.
Definition Cloth3D.h:357
float getParticleY(int index) const
Returns the particle y.
Definition Cloth3D.cpp:574
float getAirDensity() const
Return aerodynamic fluid density in kg/m³.
Definition Cloth3D.cpp:439
float getWindVelocityX() const
Return configured air velocity along X in m/s.
Definition Cloth3D.cpp:427
void setFoldStiffness(float k)
Strength of the dihedral fold clamp [0,1] (default 0.5).
Definition Cloth3D.cpp:447
void setAerodynamics(float airDensity, float dragCoefficient, float liftCoefficient)
Configure two-sided triangle aerodynamics.
Definition Cloth3D.cpp:433
Reusable, validated cloth simulation asset.
Definition ClothModel.h:73
static eve::Result< ClothModel > grid(int cols, int rows, float spacing, float originX, float originY, float originZ)
Bake a regular XZ cloth grid with its top row pinned.
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
bool isValid() const
True while the underlying Box3D world is alive.
Definition World3D.cpp:430
ClothProbeStatus pointProbeFiltered(float x, float y, float z, float radius, ClothContact3D *out, uint64_t categoryBits, uint64_t maskBits) const
Probe a cloth particle using symmetric category/mask filtering.
Definition World3D.cpp:2084
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
Optional physics backend for vehicle mobility and body attach.
Definition Climbing.h:36
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
glm::vec4 Color
Render-neutral RGBA color shared by graphics-facing modules.
Definition RenderTypes.h:8
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Duration delta
Fixed simulation duration for this step.
Definition Time.h:162
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.
uint32_t pad[2]