载入中...
搜索中...
未找到
ClothModel.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <map>
7#include <queue>
8#include <set>
9#include <string>
10#include <string_view>
11#include <tuple>
12
13namespace eve::physics {
14namespace {
15
16int64_t edgeKey(int a, int b) {
17 const int lo = std::min(a, b);
18 const int hi = std::max(a, b);
19 return (int64_t(lo) << 32) | int64_t(static_cast<uint32_t>(hi));
20}
21
22eve::Result<ClothModel> invalidModel(const char* message) {
25}
26
27bool hasExactFields(const eve::Value::Object& object, std::initializer_list<std::string_view> expected) {
28 if (object.size() != expected.size()) return false;
29 for (std::string_view name : expected)
30 if (!object.contains(std::string(name))) return false;
31 return true;
32}
33
34const eve::Value* field(const eve::Value::Object& object, std::string_view name) {
35 const auto found = object.find(std::string(name));
36 return found == object.end() ? nullptr : &found->second;
37}
38
39bool readFiniteFloat(const eve::Value& value, float& output) {
40 if (!value.isNumeric()) return false;
41 const double number = value.isDouble() ? value.asDouble() : static_cast<double>(value.asInt());
42 if (!std::isfinite(number) || number < -std::numeric_limits<float>::max() ||
43 number > std::numeric_limits<float>::max())
44 return false;
45 output = static_cast<float>(number);
46 return true;
47}
48
49constexpr size_t kMaximumDecodedParticles = 1'000'000;
50
51} // namespace
52
54 std::span<const int> triangleIndices,
55 std::span<const float> inverseMasses) {
56 if (positions.empty() || positions.size() % 3 != 0)
57 return invalidModel("cloth positions must be a non-empty packed XYZ array");
58 if (triangleIndices.empty() || triangleIndices.size() % 3 != 0)
59 return invalidModel("cloth indices must contain complete triangles");
60
61 const size_t particleCount = positions.size() / 3;
62 if (!inverseMasses.empty() && inverseMasses.size() != particleCount)
63 return invalidModel("cloth inverse-mass count must match the particle count");
64
65 ClothModel candidate;
66 candidate.particles_.reserve(particleCount);
67 for (size_t i = 0; i < particleCount; ++i) {
68 const float inverseMass = inverseMasses.empty() ? 1.f : inverseMasses[i];
69 if (!std::isfinite(positions[i * 3]) || !std::isfinite(positions[i * 3 + 1]) ||
70 !std::isfinite(positions[i * 3 + 2]) || !std::isfinite(inverseMass) || inverseMass < 0.f)
71 return invalidModel("cloth particles require finite positions and non-negative inverse masses");
72 candidate.particles_.push_back({positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2], inverseMass});
73 }
74
75 std::map<int64_t, std::vector<int>> edgeTriangles;
76 std::set<int64_t> structuralEdges;
77 for (size_t i = 0; i < triangleIndices.size(); i += 3) {
78 const int a = triangleIndices[i];
79 const int b = triangleIndices[i + 1];
80 const int c = triangleIndices[i + 2];
81 if (a < 0 || b < 0 || c < 0 || static_cast<size_t>(a) >= particleCount ||
82 static_cast<size_t>(b) >= particleCount || static_cast<size_t>(c) >= particleCount)
83 return invalidModel("cloth triangle index is out of range");
84 if (a == b || b == c || c == a) return invalidModel("cloth triangles must reference three distinct particles");
85
86 const auto& pa = candidate.particles_[static_cast<size_t>(a)];
87 const auto& pb = candidate.particles_[static_cast<size_t>(b)];
88 const auto& pc = candidate.particles_[static_cast<size_t>(c)];
89 const float abx = pb.x - pa.x, aby = pb.y - pa.y, abz = pb.z - pa.z;
90 const float acx = pc.x - pa.x, acy = pc.y - pa.y, acz = pc.z - pa.z;
91 const float nx = aby * acz - abz * acy;
92 const float ny = abz * acx - abx * acz;
93 const float nz = abx * acy - aby * acx;
94 if (nx * nx + ny * ny + nz * nz <= 1e-12f) return invalidModel("cloth triangles must have non-zero rest area");
95
96 const int triangle = static_cast<int>(candidate.triangles_.size());
97 candidate.triangles_.push_back({{a, b, c}});
98 const int edges[3][2] = {{a, b}, {b, c}, {c, a}};
99 for (const auto& edge : edges) {
100 const int64_t key = edgeKey(edge[0], edge[1]);
101 edgeTriangles[key].push_back(triangle);
102 structuralEdges.insert(key);
103 }
104 }
105
106 auto addDistance = [&](int a, int b, ClothConstraintKind kind) {
107 const auto& pa = candidate.particles_[static_cast<size_t>(a)];
108 const auto& pb = candidate.particles_[static_cast<size_t>(b)];
109 const float dx = pb.x - pa.x, dy = pb.y - pa.y, dz = pb.z - pa.z;
110 candidate.distanceConstraints_.push_back({a, b, std::sqrt(dx * dx + dy * dy + dz * dz), kind});
111 };
112 for (int64_t key : structuralEdges) {
113 addDistance(static_cast<int>(key >> 32), static_cast<int>(static_cast<uint32_t>(key)),
115 }
116
117 std::set<int64_t> bendEdges;
118 for (const auto& entry : edgeTriangles) {
119 if (entry.second.size() > 2)
120 return invalidModel("cloth topology must be manifold: an edge has more than two triangles");
121 if (entry.second.size() != 2) continue;
122 const auto& first = candidate.triangles_[static_cast<size_t>(entry.second[0])];
123 const auto& second = candidate.triangles_[static_cast<size_t>(entry.second[1])];
124 const int edgeA = static_cast<int>(entry.first >> 32);
125 const int edgeB = static_cast<int>(static_cast<uint32_t>(entry.first));
126 int oppositeA = -1;
127 int oppositeB = -1;
128 for (int vertex : first.vertices)
129 if (vertex != edgeA && vertex != edgeB) oppositeA = vertex;
130 for (int vertex : second.vertices)
131 if (vertex != edgeA && vertex != edgeB) oppositeB = vertex;
132 if (oppositeA < 0 || oppositeB < 0 || oppositeA == oppositeB)
133 return invalidModel("cloth adjacent triangles have invalid topology");
134 candidate.foldConstraints_.push_back({edgeA, edgeB, oppositeA, oppositeB});
135 if (bendEdges.insert(edgeKey(oppositeA, oppositeB)).second)
136 addDistance(oppositeA, oppositeB, ClothConstraintKind::Bend);
137 }
138
139 candidate.closed_ = std::all_of(edgeTriangles.begin(), edgeTriangles.end(),
140 [](const auto& entry) { return entry.second.size() == 2; });
141 if (candidate.closed_) {
142 double signedVolume = 0.0;
143 for (const ClothModelTriangle& triangle : candidate.triangles_) {
144 const auto& a = candidate.particles_[static_cast<size_t>(triangle.vertices[0])];
145 const auto& b = candidate.particles_[static_cast<size_t>(triangle.vertices[1])];
146 const auto& c = candidate.particles_[static_cast<size_t>(triangle.vertices[2])];
147 signedVolume += (double(a.x) * (double(b.y) * c.z - double(b.z) * c.y) +
148 double(a.y) * (double(b.z) * c.x - double(b.x) * c.z) +
149 double(a.z) * (double(b.x) * c.y - double(b.y) * c.x)) /
150 6.0;
151 }
152 if (std::abs(signedVolume) <= 1e-9) return invalidModel("closed cloth topology must have non-zero rest volume");
153 candidate.restVolume_ = static_cast<float>(signedVolume);
154 }
155
156 struct Neighbor {
157 int particle = 0;
158 float distance = 0.f;
159 };
160 std::vector<std::vector<Neighbor>> adjacency(particleCount);
161 for (int64_t key : structuralEdges) {
162 const int a = static_cast<int>(key >> 32);
163 const int b = static_cast<int>(static_cast<uint32_t>(key));
164 const auto& pa = candidate.particles_[static_cast<size_t>(a)];
165 const auto& pb = candidate.particles_[static_cast<size_t>(b)];
166 const float dx = pb.x - pa.x, dy = pb.y - pa.y, dz = pb.z - pa.z;
167 const float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
168 adjacency[static_cast<size_t>(a)].push_back({b, distance});
169 adjacency[static_cast<size_t>(b)].push_back({a, distance});
170 }
171 using QueueEntry = std::tuple<float, int, int>;
172 std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> queue;
173 std::vector<float> tetherDistance(particleCount, std::numeric_limits<float>::infinity());
174 std::vector<int> tetherAnchor(particleCount, -1);
175 for (size_t particle = 0; particle < particleCount; ++particle) {
176 if (candidate.particles_[particle].inverseMass != 0.f) continue;
177 tetherDistance[particle] = 0.f;
178 tetherAnchor[particle] = static_cast<int>(particle);
179 queue.emplace(0.f, static_cast<int>(particle), static_cast<int>(particle));
180 }
181 while (!queue.empty()) {
182 const auto [distance, anchor, particle] = queue.top();
183 queue.pop();
184 if (distance > tetherDistance[static_cast<size_t>(particle)] + 1e-6f ||
185 anchor != tetherAnchor[static_cast<size_t>(particle)])
186 continue;
187 for (const Neighbor& neighbor : adjacency[static_cast<size_t>(particle)]) {
188 const float nextDistance = distance + neighbor.distance;
189 const size_t next = static_cast<size_t>(neighbor.particle);
190 if (nextDistance + 1e-6f < tetherDistance[next] ||
191 (std::abs(nextDistance - tetherDistance[next]) <= 1e-6f &&
192 (tetherAnchor[next] < 0 || anchor < tetherAnchor[next]))) {
193 tetherDistance[next] = nextDistance;
194 tetherAnchor[next] = anchor;
195 queue.emplace(nextDistance, anchor, neighbor.particle);
196 }
197 }
198 }
199 for (size_t particle = 0; particle < particleCount; ++particle)
200 if (candidate.particles_[particle].inverseMass > 0.f && tetherAnchor[particle] >= 0)
201 candidate.tetherConstraints_.push_back(
202 {static_cast<int>(particle), tetherAnchor[particle], tetherDistance[particle]});
203
204 return eve::Result<ClothModel>::success(std::move(candidate));
205}
206
207eve::Result<ClothModel> ClothModel::grid(int cols, int rows, float spacing, float originX, float originY,
208 float originZ) {
209 if (cols < 2 || rows < 2) return invalidModel("cloth grid dimensions must be at least two");
210 if (!std::isfinite(spacing) || spacing <= 0.f)
211 return invalidModel("cloth grid spacing must be finite and positive");
212 if (!std::isfinite(originX) || !std::isfinite(originY) || !std::isfinite(originZ))
213 return invalidModel("cloth grid origin must be finite");
214 if (static_cast<size_t>(cols) > static_cast<size_t>(std::numeric_limits<int>::max()) / static_cast<size_t>(rows))
215 return invalidModel("cloth grid particle count exceeds the supported index range");
216
217 std::vector<float> positions;
218 std::vector<float> inverseMasses;
219 std::vector<int> indices;
220 positions.reserve(static_cast<size_t>(cols * rows * 3));
221 inverseMasses.reserve(static_cast<size_t>(cols * rows));
222 for (int row = 0; row < rows; ++row) {
223 for (int col = 0; col < cols; ++col) {
224 positions.push_back(originX + static_cast<float>(col) * spacing);
225 positions.push_back(originY);
226 positions.push_back(originZ + static_cast<float>(row) * spacing);
227 inverseMasses.push_back(row == 0 ? 0.f : 1.f);
228 }
229 }
230 for (int row = 0; row + 1 < rows; ++row) {
231 for (int col = 0; col + 1 < cols; ++col) {
232 const int a = row * cols + col;
233 const int b = a + 1;
234 const int d = (row + 1) * cols + col;
235 const int c = d + 1;
236 indices.insert(indices.end(), {a, d, b, d, c, b});
237 }
238 }
239 auto result = fromTriangles(positions, indices, inverseMasses);
240 if (!result) return result;
241 ClothModel model = std::move(result).takeValue();
242
243 // Preserve the established grid behavior and solver order exactly: PBD
244 // projection order affects finite-iteration results. Arbitrary meshes use
245 // the canonical edge order produced by fromTriangles above.
246 model.distanceConstraints_.clear();
247 auto addGridDistance = [&](int a, int b, ClothConstraintKind kind) {
248 const auto& pa = model.particles_[static_cast<size_t>(a)];
249 const auto& pb = model.particles_[static_cast<size_t>(b)];
250 const float dx = pb.x - pa.x, dy = pb.y - pa.y, dz = pb.z - pa.z;
251 model.distanceConstraints_.push_back({a, b, std::sqrt(dx * dx + dy * dy + dz * dz), kind});
252 };
253 for (int row = 0; row < rows; ++row) {
254 for (int col = 0; col < cols; ++col) {
255 const int particle = row * cols + col;
256 if (col + 1 < cols) addGridDistance(particle, particle + 1, ClothConstraintKind::Structural);
257 if (row + 1 < rows) addGridDistance(particle, particle + cols, ClothConstraintKind::Structural);
258 if (col + 1 < cols && row + 1 < rows)
259 addGridDistance(particle, particle + cols + 1, ClothConstraintKind::Shear);
260 if (col > 0 && row + 1 < rows) addGridDistance(particle, particle + cols - 1, ClothConstraintKind::Shear);
261 if (col + 2 < cols) addGridDistance(particle, particle + 2, ClothConstraintKind::Bend);
262 if (row + 2 < rows) addGridDistance(particle, particle + cols * 2, ClothConstraintKind::Bend);
263 }
264 }
265 model.gridCols_ = cols;
266 model.gridRows_ = rows;
267 model.gridSpacing_ = spacing;
268 return eve::Result<ClothModel>::success(std::move(model));
269}
270
273 eve::Value::Array inverseMasses;
275 positions.reserve(particles_.size() * 3);
276 inverseMasses.reserve(particles_.size());
277 triangles.reserve(triangles_.size() * 3);
278 for (const ClothModelParticle& particle : particles_) {
279 positions.emplace_back(particle.x);
280 positions.emplace_back(particle.y);
281 positions.emplace_back(particle.z);
282 inverseMasses.emplace_back(particle.inverseMass);
283 }
284 for (const ClothModelTriangle& triangle : triangles_) {
285 triangles.emplace_back(triangle.vertices[0]);
286 triangles.emplace_back(triangle.vertices[1]);
287 triangles.emplace_back(triangle.vertices[2]);
288 }
289
290 eve::Value gridMetadata;
291 if (gridCols_ > 0 && gridRows_ > 0) {
292 gridMetadata = eve::Value::Object{
293 {"cols", eve::Value(gridCols_)}, {"rows", eve::Value(gridRows_)}, {"spacing", eve::Value(gridSpacing_)}};
294 }
295 return eve::Value::Object{{"grid", std::move(gridMetadata)},
296 {"inverseMasses", eve::Value(std::move(inverseMasses))},
297 {"positions", eve::Value(std::move(positions))},
298 {"schema", eve::Value("eve.cloth-model")},
299 {"schemaVersion", eve::Value(static_cast<int>(SchemaVersion))},
300 {"triangles", eve::Value(std::move(triangles))}};
301}
302
304
306 auto value = eve::Value::fromJson(json);
307 if (!value) return eve::Result<ClothModel>::failure(value.status());
308 return fromValue(value.value());
309}
310
312 const auto* root = value.getIf<eve::Value::Object>();
313 if (!root || !hasExactFields(*root, {"schema", "schemaVersion", "positions", "inverseMasses", "triangles", "grid"}))
315 eve::DiagnosticCode::ParseError, "cloth model root has missing or unknown fields", {}));
316
317 const eve::Value* schema = field(*root, "schema");
318 const eve::Value* version = field(*root, "schemaVersion");
319 if (!schema || !schema->isString() || schema->asString() != "eve.cloth-model")
321 eve::DiagnosticCode::ParseError, "cloth model schema must be eve.cloth-model", "schema"));
322 if (!version || !version->isInt64())
324 eve::DiagnosticCode::ParseError, "cloth model schemaVersion must be an integer", "schemaVersion"));
325 if (version->asInt() != SchemaVersion)
327 eve::DiagnosticCode::UnknownVersion, "cloth model schema version is not supported", "schemaVersion"));
328
329 const auto* encodedPositions = field(*root, "positions")->getIf<eve::Value::Array>();
330 const auto* encodedMasses = field(*root, "inverseMasses")->getIf<eve::Value::Array>();
331 const auto* encodedTriangles = field(*root, "triangles")->getIf<eve::Value::Array>();
332 if (!encodedPositions || encodedPositions->empty() || encodedPositions->size() % 3 != 0 ||
333 encodedPositions->size() / 3 > kMaximumDecodedParticles || !encodedMasses ||
334 encodedMasses->size() != encodedPositions->size() / 3 || !encodedTriangles || encodedTriangles->empty() ||
335 encodedTriangles->size() % 3 != 0)
337 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "cloth model arrays have invalid sizes", {}));
338
339 std::vector<float> positions;
340 std::vector<float> inverseMasses;
341 std::vector<int> triangles;
342 positions.reserve(encodedPositions->size());
343 inverseMasses.reserve(encodedMasses->size());
344 triangles.reserve(encodedTriangles->size());
345 for (const eve::Value& encoded : *encodedPositions) {
346 float decoded = 0.f;
347 if (!readFiniteFloat(encoded, decoded))
349 eve::DiagnosticCode::ParseError, "cloth position must be a finite float", "positions"));
350 positions.push_back(decoded);
351 }
352 for (const eve::Value& encoded : *encodedMasses) {
353 float decoded = 0.f;
354 if (!readFiniteFloat(encoded, decoded) || decoded < 0.f)
357 "cloth inverse mass must be finite and non-negative", "inverseMasses"));
358 inverseMasses.push_back(decoded);
359 }
360 for (const eve::Value& encoded : *encodedTriangles) {
361 if (!encoded.isInt64() || encoded.asInt() < 0 || encoded.asInt() > std::numeric_limits<int>::max())
363 eve::DiagnosticCode::ParseError, "cloth triangle index must be a non-negative integer", "triangles"));
364 triangles.push_back(static_cast<int>(encoded.asInt()));
365 }
366
367 auto decoded = fromTriangles(positions, triangles, inverseMasses);
368 if (!decoded) return decoded;
369
370 const eve::Value* gridValue = field(*root, "grid");
371 if (gridValue->isNull()) return decoded;
372 const auto* gridObject = gridValue->getIf<eve::Value::Object>();
373 if (!gridObject || !hasExactFields(*gridObject, {"cols", "rows", "spacing"}))
375 eve::DiagnosticCode::ParseError, "cloth grid metadata has missing or unknown fields", "grid"));
376 const eve::Value* colsValue = field(*gridObject, "cols");
377 const eve::Value* rowsValue = field(*gridObject, "rows");
378 const eve::Value* spacingValue = field(*gridObject, "spacing");
379 float spacing = 0.f;
380 if (!colsValue || !colsValue->isInt64() || !rowsValue || !rowsValue->isInt64() || colsValue->asInt() < 2 ||
381 colsValue->asInt() > std::numeric_limits<int>::max() || rowsValue->asInt() < 2 ||
382 rowsValue->asInt() > std::numeric_limits<int>::max() || !spacingValue ||
383 !readFiniteFloat(*spacingValue, spacing) || spacing <= 0.f)
385 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "cloth grid metadata values are invalid", "grid"));
386
387 const int cols = static_cast<int>(colsValue->asInt());
388 const int rows = static_cast<int>(rowsValue->asInt());
389 auto canonical = grid(cols, rows, spacing, positions[0], positions[1], positions[2]);
390 if (!canonical) return eve::Result<ClothModel>::failure(canonical.status());
391 if (canonical.value().particles().size() != decoded.value().particles().size() ||
392 canonical.value().triangles().size() != decoded.value().triangles().size())
394 eve::DiagnosticCode::Conflict, "cloth grid metadata disagrees with its topology", "grid"));
395 for (size_t i = 0; i < canonical.value().particles().size(); ++i) {
396 const auto& expected = canonical.value().particles()[i];
397 const auto& actual = decoded.value().particles()[i];
398 if (std::abs(expected.x - actual.x) > 1e-5f || std::abs(expected.y - actual.y) > 1e-5f ||
399 std::abs(expected.z - actual.z) > 1e-5f || expected.inverseMass != actual.inverseMass)
401 eve::DiagnosticCode::Conflict, "cloth grid metadata disagrees with its particles", "grid"));
402 }
403 for (size_t i = 0; i < canonical.value().triangles().size(); ++i)
404 for (int vertex = 0; vertex < 3; ++vertex)
405 if (canonical.value().triangles()[i].vertices[vertex] != decoded.value().triangles()[i].vertices[vertex])
407 eve::DiagnosticCode::Conflict, "cloth grid metadata disagrees with triangle ordering", "grid"));
408 return canonical;
409}
410
411} // namespace eve::physics
double value
int root
Definition AnimSmr.cpp:119
std::string output
Vec3 anchor
Definition CaveMesh.cpp:90
float nx
float nz
float ny
int rows
int cols
int triangle
std::string message
std::uint32_t key
std::vector< std::uint32_t > indices
std::vector< float > positions
std::int32_t second
std::int32_t c
std::int32_t first
TokenKind kind
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
float d
glm::mat4 model
const RoadEdge * edge
double number
bool found
std::string string
float dz
float dy
float dx
int spacing
Json object
float size
Definition TreeMesh.cpp:156
std::vector< int > edges
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
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
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
const std::string & asString() const
Return the string payload; the caller must have checked the kind.
Definition Value.cpp:87
bool isInt64() const noexcept
Return true when this value is an Int64.
Definition Value.h:91
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
bool isString() const noexcept
Return true when this value is a string.
Definition Value.h:95
std::int64_t asInt() const
Return the Int64 payload; the caller must have checked the kind.
Definition Value.cpp:69
std::vector< Value > Array
Definition Value.h:33
bool isNull() const noexcept
Return true when this value is null.
Definition Value.h:87
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
const T * getIf() const
Return an immediately borrowed typed value.
Reusable, validated cloth simulation asset.
Definition ClothModel.h:73
static eve::Result< ClothModel > fromValue(const eve::Value &value)
Decode strict eve.cloth-model/1 data; unknown fields are rejected.
eve::Result< std::string > toJson() const
Serialize deterministic compact eve.cloth-model/1 JSON.
static eve::Result< ClothModel > fromJson(std::string_view json)
Parse and decode strict UTF-8 eve.cloth-model/1 JSON.
std::span< const ClothModelParticle > particles() const noexcept
Read-only particles, valid until this model is destroyed or assigned.
Definition ClothModel.h:119
eve::Value toValue() const
Encode canonical owning eve.cloth-model/1 data.
static eve::Result< ClothModel > fromTriangles(std::span< const float > positions, std::span< const int > triangleIndices, std::span< const float > inverseMasses={})
Bake an arbitrary indexed triangle mesh into a cloth model.
std::span< const ClothModelTriangle > triangles() const noexcept
Read-only triangles, valid until this model is destroyed or assigned.
Definition ClothModel.h:121
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.
Optional physics backend for vehicle mobility and body attach.
Definition Climbing.h:36
ClothConstraintKind
Type of a precomputed distance constraint in a cloth model.
Definition ClothModel.h:17
const EditorValue * field(const EditorValue &value, const char *name)
Immutable rest-space particle stored by a ClothModel.
Definition ClothModel.h:24
Triangle indices into a ClothModel particle array.
Definition ClothModel.h:32