载入中...
搜索中...
未找到
VolumeFluidColliders.cpp
浏览该文件的文档.
1#include "fluids/VolumeFluidInternal.inc"
2
3namespace eve::fluids {
4Result<void> VolumeFluid::setColliders(std::span<const VolumeFluidCollider> input) {
5 if (input.size() > 1024 || uint64_t(input.size()) * impl_->state.size() > 4000000)
6 return invalid("Analytic collider count or candidate budget exceeded");
7 for (size_t i = 0; i < input.size(); ++i) {
8 const auto& c = input[i];
9 if (!finite(c.center) || !finite(c.halfExtent) || !finite(c.velocity) || glm::length(c.velocity) > 100.f ||
10 !finite(c.angularVelocity) || glm::length(c.angularVelocity) > 1000.f || !finite(c.rotation) ||
11 std::abs(glm::dot(c.rotation, c.rotation) - 1.f) > .001f ||
12 glm::any(glm::lessThanEqual(c.halfExtent, glm::vec3(0.f))) || !range(c.radius, 0.001f, 10000.f) ||
13 !validColliderMaterial(c) || !validFilter(c.collisionFilter) || (c.isTrigger && c.solidify) ||
16 return invalid("Invalid collider sample");
17 for (size_t j = 0; j < i; ++j)
18 if (input[j].label == c.label) return invalid("Duplicate collider label");
19 if (std::any_of(impl_->sdfColliders.begin(), impl_->sdfColliders.end(),
20 [&](const auto& sdf) { return sdf.label == c.label; }))
21 return invalid("Collider labels must be unique across analytic and SDF colliders");
22 if (std::any_of(impl_->heightFieldColliders.begin(), impl_->heightFieldColliders.end(),
23 [&](const auto& terrain) { return terrain.label == c.label; }))
24 return invalid("Collider labels must be unique across all collider kinds");
25 }
26 std::vector<VolumeFluidCollider> replacement(input.begin(), input.end());
27 std::vector<glm::mat3> rotations;
28 rotations.reserve(input.size());
29 for (const auto& c : input)
30 rotations.push_back(
31 glm::mat3_cast(glm::normalize(glm::quat(c.rotation.w, c.rotation.x, c.rotation.y, c.rotation.z))));
32 std::vector<unsigned> labels;
33 if (!impl_->attachments.empty()) {
34 labels.reserve(input.size());
35 for (const auto& c : input) labels.push_back(c.label);
36 std::sort(labels.begin(), labels.end());
37 }
38 impl_->colliders.swap(replacement);
39 impl_->colliderRotations.swap(rotations);
40 std::erase_if(impl_->attachments,
41 [&](const auto& a) { return !std::binary_search(labels.begin(), labels.end(), a.colliderLabel); });
42 impl_->attachmentLabels.clear();
43 impl_->attachmentOrientationConstraints.clear();
44 impl_->releaseMissingGrabbers();
45 return Result<void>::success();
46}
47
48Result<unsigned> VolumeFluid::bindStaticParticles(std::span<const unsigned> particleIndices, unsigned colliderLabel,
49 bool constrainOrientation) {
50 return bindParticles(particleIndices, colliderLabel, constrainOrientation, false, 0.f, 1e12f);
51}
52
53Result<unsigned> VolumeFluid::bindDynamicParticles(std::span<const unsigned> particleIndices, unsigned colliderLabel,
54 float compliance, float breakThreshold, bool constrainOrientation) {
55 return bindParticles(particleIndices, colliderLabel, constrainOrientation, true, compliance, breakThreshold);
56}
57
58Result<unsigned> VolumeFluid::bindParticles(std::span<const unsigned> particleIndices, unsigned colliderLabel,
59 bool constrainOrientation, bool dynamic, float compliance,
60 float breakThreshold) {
61 using Output = Result<unsigned>;
62 if (particleIndices.empty() || particleIndices.size() > impl_->state.size() || !range(compliance, 0.f, 1000000.f) ||
63 !range(breakThreshold, 1e-6f, 1e12f))
65 "Invalid particle attachment group or constraint settings",
66 "fluids.volume.attachment"));
67 const auto collider = std::find_if(impl_->colliders.begin(), impl_->colliders.end(),
68 [&](const auto& item) { return item.label == colliderLabel; });
69 if (collider == impl_->colliders.end())
71 "Static attachment target collider is missing",
72 "fluids.volume.attachment"));
73 std::vector<unsigned> ordered(particleIndices.begin(), particleIndices.end());
74 std::sort(ordered.begin(), ordered.end());
75 if (ordered.back() >= impl_->state.size() || std::adjacent_find(ordered.begin(), ordered.end()) != ordered.end())
77 "Static attachment indices must be unique and current",
78 "fluids.volume.attachment"));
79 for (const auto& attachment : impl_->attachments)
80 if (std::binary_search(ordered.begin(), ordered.end(), attachment.particleIndex))
81 return Output::failure(Diagnostic::error(
82 DiagnosticCode::InvalidArgument, "Particle already owns an attachment", "fluids.volume.attachment"));
83
84 const auto colliderQ = glm::normalize(
85 glm::quat(collider->rotation.w, collider->rotation.x, collider->rotation.y, collider->rotation.z));
86 const auto inverseRotation = glm::transpose(glm::mat3_cast(colliderQ));
87 std::vector<VolumeFluidAttachment> additions;
88 additions.reserve(ordered.size());
89 for (const unsigned index : ordered) {
90 const auto particleQ = particleQuaternion(impl_->state[index]);
91 const auto localQ = glm::normalize(glm::conjugate(colliderQ) * particleQ);
92 additions.push_back({index,
93 colliderLabel,
94 inverseRotation * (impl_->state[index].position - collider->center),
95 {localQ.x, localQ.y, localQ.z, localQ.w},
96 constrainOrientation,
97 dynamic,
98 compliance,
99 breakThreshold});
100 }
101 impl_->attachments.reserve(impl_->attachments.size() + additions.size());
102 impl_->attachments.insert(impl_->attachments.end(), additions.begin(), additions.end());
103 impl_->attachmentLabels.clear();
104 impl_->attachmentOrientationConstraints.clear();
105 impl_->contacts.clear();
106 impl_->attachmentReactions.clear();
107 return Output::success(unsigned(additions.size()));
108}
109
110Result<unsigned> VolumeFluid::unbindStaticParticles(std::span<const unsigned> particleIndices) {
111 using Output = Result<unsigned>;
112 if (particleIndices.empty() || particleIndices.size() > impl_->state.size())
113 return Output::failure(Diagnostic::error(
114 DiagnosticCode::InvalidArgument, "Invalid static attachment particle group", "fluids.volume.attachment"));
115 std::vector<unsigned> ordered(particleIndices.begin(), particleIndices.end());
116 std::sort(ordered.begin(), ordered.end());
117 if (ordered.back() >= impl_->state.size() || std::adjacent_find(ordered.begin(), ordered.end()) != ordered.end())
119 "Static attachment indices must be unique and current",
120 "fluids.volume.attachment"));
121 const auto oldSize = impl_->attachments.size();
122 std::erase_if(impl_->attachments, [&](const auto& attachment) {
123 return std::binary_search(ordered.begin(), ordered.end(), attachment.particleIndex);
124 });
125 impl_->attachmentLabels.clear();
126 impl_->attachmentOrientationConstraints.clear();
127 impl_->contacts.clear();
128 impl_->attachmentReactions.clear();
129 return Output::success(unsigned(oldSize - impl_->attachments.size()));
130}
131
132Result<void> VolumeFluid::setStitches(std::span<const VolumeFluidStitch> stitches) {
133 if (stitches.size() > 65536) return invalid("Too many particle stitches");
134 std::vector<VolumeFluidStitch> candidate(stitches.begin(), stitches.end());
135 for (auto& stitch : candidate) {
136 if (stitch.particleIndex1 >= impl_->state.size() || stitch.particleIndex2 >= impl_->state.size() ||
137 stitch.particleIndex1 == stitch.particleIndex2 || !range(stitch.compliance, 0.f, 1000000.f))
138 return invalid("Invalid particle stitch");
139 if (stitch.particleIndex2 < stitch.particleIndex1) std::swap(stitch.particleIndex1, stitch.particleIndex2);
140 }
141 std::sort(candidate.begin(), candidate.end(), [](const auto& a, const auto& b) {
142 return std::tie(a.particleIndex1, a.particleIndex2) < std::tie(b.particleIndex1, b.particleIndex2);
143 });
144 if (std::adjacent_find(candidate.begin(), candidate.end(), [](const auto& a, const auto& b) {
145 return a.particleIndex1 == b.particleIndex1 && a.particleIndex2 == b.particleIndex2;
146 }) != candidate.end())
147 return invalid("Duplicate particle stitch");
148 impl_->stitches.swap(candidate);
149 return Result<void>::success();
150}
151
152Result<void> VolumeFluid::setSdfColliders(std::span<const VolumeFluidSdfCollider> input) {
153 if (input.size() > 16 || uint64_t(input.size()) * impl_->state.size() > 4000000)
154 return invalid("SDF collider count or candidate budget exceeded");
155 uint64_t totalSamples = 0;
156 std::vector<glm::mat3> rotations;
157 rotations.reserve(input.size());
158 for (size_t i = 0; i < input.size(); ++i) {
159 const auto& c = input[i];
160 const auto& sdf = c.sdf;
161 if (sdf.dims.x < 2 || sdf.dims.y < 2 || sdf.dims.z < 2 || sdf.dims.x > 256 || sdf.dims.y > 256 ||
162 sdf.dims.z > 256 || !range(sdf.cellSize, .0005f, 1000.f) || !finite(sdf.origin) || !finite(c.position) ||
163 !finite(c.rotation) || std::abs(glm::dot(c.rotation, c.rotation) - 1.f) > .001f ||
164 !range(c.scale, .0001f, 10000.f) || !finite(c.velocity) || glm::length(c.velocity) > 100.f ||
165 !finite(c.angularVelocity) || glm::length(c.angularVelocity) > 1000.f || !validColliderMaterial(c) ||
166 !validFilter(c.collisionFilter))
167 return invalid("Invalid SDF collider");
168 const uint64_t samples = uint64_t(sdf.dims.x) * uint64_t(sdf.dims.y) * uint64_t(sdf.dims.z);
169 totalSamples += samples;
170 if (samples != sdf.distances.size() || totalSamples > 4000000 ||
171 std::any_of(sdf.distances.begin(), sdf.distances.end(),
172 [](float value) { return !std::isfinite(value) || std::abs(value) > 10000.f; }))
173 return invalid("Invalid SDF collider samples");
174 for (size_t j = 0; j < i; ++j)
175 if (input[j].label == c.label) return invalid("Duplicate SDF collider label");
176 if (std::any_of(impl_->colliders.begin(), impl_->colliders.end(),
177 [&](const auto& analytic) { return analytic.label == c.label; }))
178 return invalid("Collider labels must be unique across analytic and SDF colliders");
179 if (std::any_of(impl_->heightFieldColliders.begin(), impl_->heightFieldColliders.end(),
180 [&](const auto& terrain) { return terrain.label == c.label; }))
181 return invalid("Collider labels must be unique across all collider kinds");
182 const auto rotation =
183 glm::mat3_cast(glm::normalize(glm::quat(c.rotation.w, c.rotation.x, c.rotation.y, c.rotation.z)));
184 if (c.inverted) {
185 const auto minimum = sdf.origin;
186 const auto maximum = sdf.origin + glm::vec3(sdf.dims - glm::ivec3(1)) * sdf.cellSize;
187 for (unsigned corner = 0; corner < 8; ++corner) {
188 const glm::vec3 world{corner & 1 ? impl_->settings.maximum.x : impl_->settings.minimum.x,
189 corner & 2 ? impl_->settings.maximum.y : impl_->settings.minimum.y,
190 corner & 4 ? impl_->settings.maximum.z : impl_->settings.minimum.z};
191 const auto local = glm::transpose(rotation) * (world - c.position) / c.scale;
192 if (glm::any(glm::lessThan(local, minimum)) || glm::any(glm::greaterThan(local, maximum)))
193 return invalid("Inverted SDF domain must contain solver bounds");
194 }
195 }
196 rotations.push_back(rotation);
197 }
198 std::vector<VolumeFluidSdfCollider> replacement(input.begin(), input.end());
199 impl_->sdfColliders.swap(replacement);
200 impl_->sdfColliderRotations.swap(rotations);
201 impl_->releaseMissingGrabbers();
202 return Result<void>::success();
203}
204
205Result<void> VolumeFluid::setHeightFieldColliders(std::span<const VolumeFluidHeightFieldCollider> input) {
206 if (input.size() > 16 || uint64_t(input.size()) * impl_->state.size() > 4000000)
207 return invalid("Height-field collider count or candidate budget exceeded");
208 uint64_t totalSamples = 0;
209 std::vector<glm::mat3> rotations;
210 rotations.reserve(input.size());
211 for (size_t i = 0; i < input.size(); ++i) {
212 const auto& c = input[i];
213 if (c.resolution.x < 2 || c.resolution.y < 2 || c.resolution.x > 2048 || c.resolution.y > 2048 ||
214 !finite(c.size) || glm::any(glm::lessThanEqual(c.size, glm::vec3(0.f))) || !finite(c.position) ||
215 !finite(c.rotation) || std::abs(glm::dot(c.rotation, c.rotation) - 1.f) > .001f || !finite(c.velocity) ||
216 glm::length(c.velocity) > 100.f || !finite(c.angularVelocity) || glm::length(c.angularVelocity) > 1000.f ||
217 !validColliderMaterial(c) || !validFilter(c.collisionFilter))
218 return invalid("Invalid height-field collider");
219 const uint64_t samples = uint64_t(c.resolution.x) * uint64_t(c.resolution.y);
220 totalSamples += samples;
221 if (samples != c.heights.size() || totalSamples > 4000000 ||
222 std::any_of(c.heights.begin(), c.heights.end(),
223 [](float h) { return !std::isfinite(h) || h < 0.f || h > 1.f; }))
224 return invalid("Invalid height-field samples or sample budget exceeded");
225 for (size_t j = 0; j < i; ++j)
226 if (input[j].label == c.label) return invalid("Duplicate height-field collider label");
227 if (std::any_of(impl_->colliders.begin(), impl_->colliders.end(),
228 [&](const auto& analytic) { return analytic.label == c.label; }) ||
229 std::any_of(impl_->sdfColliders.begin(), impl_->sdfColliders.end(),
230 [&](const auto& sdf) { return sdf.label == c.label; }))
231 return invalid("Collider labels must be unique across all collider kinds");
232 rotations.push_back(
233 glm::mat3_cast(glm::normalize(glm::quat(c.rotation.w, c.rotation.x, c.rotation.y, c.rotation.z))));
234 }
235 std::vector<VolumeFluidHeightFieldCollider> replacement(input.begin(), input.end());
236 impl_->heightFieldColliders.swap(replacement);
237 impl_->heightFieldColliderRotations.swap(rotations);
238 impl_->releaseMissingGrabbers();
239 return Result<void>::success();
240}
241
242Result<void> VolumeFluid::updateSdfColliderPoses(std::span<const VolumeFluidSdfPose> poses) {
243 if (poses.size() > 16) return invalid("Too many SDF collider pose updates");
244 std::vector<size_t> indices;
245 indices.reserve(poses.size());
246 std::vector<glm::mat3> rotations;
247 rotations.reserve(poses.size());
248 for (size_t i = 0; i < poses.size(); ++i) {
249 const auto& pose = poses[i];
250 if (!finite(pose.position) || !finite(pose.rotation) ||
251 std::abs(glm::dot(pose.rotation, pose.rotation) - 1.f) > .001f || !range(pose.scale, .0001f, 10000.f) ||
252 !finite(pose.velocity) || glm::length(pose.velocity) > 100.f || !finite(pose.angularVelocity) ||
253 glm::length(pose.angularVelocity) > 1000.f)
254 return invalid("Invalid SDF collider pose");
255 for (size_t j = 0; j < i; ++j)
256 if (poses[j].label == pose.label) return invalid("Duplicate SDF collider pose label");
257 const auto found = std::find_if(impl_->sdfColliders.begin(), impl_->sdfColliders.end(),
258 [&](const auto& collider) { return collider.label == pose.label; });
259 if (found == impl_->sdfColliders.end()) return invalid("Unknown SDF collider pose label");
260 const size_t index = size_t(found - impl_->sdfColliders.begin());
261 const auto rotation = glm::mat3_cast(
262 glm::normalize(glm::quat(pose.rotation.w, pose.rotation.x, pose.rotation.y, pose.rotation.z)));
263 if (found->inverted) {
264 const auto minimum = found->sdf.origin;
265 const auto maximum = minimum + glm::vec3(found->sdf.dims - glm::ivec3(1)) * found->sdf.cellSize;
266 for (unsigned corner = 0; corner < 8; ++corner) {
267 const glm::vec3 world{corner & 1 ? impl_->settings.maximum.x : impl_->settings.minimum.x,
268 corner & 2 ? impl_->settings.maximum.y : impl_->settings.minimum.y,
269 corner & 4 ? impl_->settings.maximum.z : impl_->settings.minimum.z};
270 const auto local = glm::transpose(rotation) * (world - pose.position) / pose.scale;
271 if (glm::any(glm::lessThan(local, minimum)) || glm::any(glm::greaterThan(local, maximum)))
272 return invalid("Updated inverted SDF domain must contain solver bounds");
273 }
274 }
275 indices.push_back(index);
276 rotations.push_back(rotation);
277 }
278 for (size_t i = 0; i < poses.size(); ++i) {
279 auto& collider = impl_->sdfColliders[indices[i]];
280 const auto& pose = poses[i];
281 collider.position = pose.position;
282 collider.rotation = pose.rotation;
283 collider.scale = pose.scale;
284 collider.velocity = pose.velocity;
285 collider.angularVelocity = pose.angularVelocity;
286 impl_->sdfColliderRotations[indices[i]] = rotations[i];
287 }
288 return Result<void>::success();
289}
290
291std::vector<VolumeFluidContact> VolumeFluid::contacts() const { return impl_->contacts; }
292
293std::span<const VolumeFluidAttachmentReaction> VolumeFluid::attachmentReactions() const {
294 return impl_->attachmentReactions;
295}
296
298 if (!finite(center) || !range(radius, 0.f, 10000.f))
300 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid overlap sphere", "fluids.volume.overlap"));
301 std::vector<VolumeFluidParticle> hits;
302 for (const auto& p : impl_->state)
303 if (glm::length(p.position - center) <= radius) hits.push_back(p);
304 return Result<std::vector<VolumeFluidParticle>>::success(std::move(hits));
305}
306
308 glm::vec4 rotation) const {
309 if (!finite(center) || !finite(halfExtent) || glm::any(glm::lessThan(halfExtent, glm::vec3(0.f))) ||
310 glm::any(glm::greaterThan(halfExtent, glm::vec3(10000.f))) || !finite(rotation) ||
311 std::abs(glm::dot(rotation, rotation) - 1.f) > 0.001f)
313 Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid overlap box", "fluids.volume.overlapBox"));
314 const auto inverseRotation =
315 glm::transpose(glm::mat3_cast(glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z))));
316 std::vector<VolumeFluidParticle> hits;
317 for (const auto& particle : impl_->state) {
318 const auto local = inverseRotation * (particle.position - center);
319 if (glm::all(glm::lessThanEqual(glm::abs(local), halfExtent))) hits.push_back(particle);
320 }
321 return Result<std::vector<VolumeFluidParticle>>::success(std::move(hits));
322}
323
325 unsigned maxHits, unsigned phaseMask) const {
327 const auto failure = [](const char* message) {
328 return Output::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.raycast"));
329 };
330 const float directionLength = glm::length(direction);
331 if (!finite(origin) || !finite(direction) || !std::isfinite(directionLength) || directionLength <= 1e-7f ||
332 !range(maxDistance, 0.f, 10000.f) || maxHits == 0 || maxHits > 4096 || phaseMask == 0 ||
333 (phaseMask & ~0x0fu) != 0)
334 return failure("Invalid ray, distance, hit limit or phase mask");
335 if (impl_->state.size() > 65536) return failure("Ray query exceeds 65536-particle work budget");
336 direction /= directionLength;
337 struct Farther {
338 bool operator()(const VolumeFluidRayHit& a, const VolumeFluidRayHit& b) const {
339 if (a.distance != b.distance) return a.distance < b.distance;
340 return a.particleIndex < b.particleIndex;
341 }
342 };
343 std::priority_queue<VolumeFluidRayHit, std::vector<VolumeFluidRayHit>, Farther> nearest;
344 for (size_t i = 0; i < impl_->state.size(); ++i) {
345 const auto& particle = impl_->state[i];
346 const unsigned phase = unsigned(particle.material.phase);
347 if ((phaseMask & (1u << phase)) == 0) continue;
348 float distance = 0.f;
349 glm::vec3 normal;
350 if (!rayEllipsoid(particle, origin, direction, maxDistance, distance, normal)) continue;
351 const auto point = origin + direction * distance;
352 VolumeFluidRayHit hit{unsigned(i), distance, point, normal, particle};
353 if (nearest.size() < maxHits)
354 nearest.push(std::move(hit));
355 else {
356 const auto& worst = nearest.top();
357 if (distance > worst.distance || (distance == worst.distance && i >= worst.particleIndex)) continue;
358 nearest.pop();
359 nearest.push(std::move(hit));
360 }
361 }
362 std::vector<VolumeFluidRayHit> hits(nearest.size());
363 for (size_t i = hits.size(); i > 0; --i) {
364 hits[i - 1] = nearest.top();
365 nearest.pop();
366 }
367 return Output::success(std::move(hits));
368}
369
371 float contactOffset, float maxDistance,
372 unsigned maxHits, unsigned phaseMask,
373 unsigned collisionFilter) const {
375 const auto failure = [](const char* message) {
376 return Output::failure(
377 Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.querySphere"));
378 };
379 if (!finite(center) || !range(radius, 0.f, 10000.f) || !range(contactOffset, 0.f, 10000.f) ||
380 !range(maxDistance, 0.f, 10000.f) || maxHits == 0 || maxHits > 4096 || phaseMask == 0 ||
381 (phaseMask & ~0x0fu) != 0 || !validFilter(collisionFilter))
382 return failure("Invalid sphere query, distance, hit limit, phase mask or collision filter");
383 if (impl_->state.size() > 65536) return failure("Sphere query exceeds 65536-particle work budget");
384 struct Farther {
385 bool operator()(const VolumeFluidDistanceHit& a, const VolumeFluidDistanceHit& b) const {
386 if (a.distance != b.distance) return a.distance < b.distance;
387 return a.particleIndex < b.particleIndex;
388 }
389 };
390 std::priority_queue<VolumeFluidDistanceHit, std::vector<VolumeFluidDistanceHit>, Farther> nearest;
391 for (size_t i = 0; i < impl_->state.size(); ++i) {
392 const auto& particle = impl_->state[i];
393 const unsigned phase = unsigned(particle.material.phase);
394 if ((phaseMask & (1u << phase)) == 0 || !filtersMatch(collisionFilter, particle.collisionFilter)) continue;
395 const auto offset = particle.position - center;
396 const float centerDistance = glm::length(offset);
397 const auto normal = centerDistance > 1e-7f ? offset / centerDistance : glm::vec3(0.f, 1.f, 0.f);
398 const float distance = centerDistance - radius - contactOffset - ellipsoidRadius(particle, normal);
399 if (distance > maxDistance) continue;
400 VolumeFluidDistanceHit hit{unsigned(i), distance, center + normal * (radius + contactOffset), normal, particle};
401 if (nearest.size() < maxHits)
402 nearest.push(std::move(hit));
403 else {
404 const auto& worst = nearest.top();
405 if (distance > worst.distance || (distance == worst.distance && i >= worst.particleIndex)) continue;
406 nearest.pop();
407 nearest.push(std::move(hit));
408 }
409 }
410 std::vector<VolumeFluidDistanceHit> hits(nearest.size());
411 for (size_t i = hits.size(); i > 0; --i) {
412 hits[i - 1] = nearest.top();
413 nearest.pop();
414 }
415 return Output::success(std::move(hits));
416}
417
419 glm::vec4 rotation, float contactOffset,
420 float maxDistance, unsigned maxHits,
421 unsigned phaseMask, unsigned collisionFilter) const {
423 const auto failure = [](const char* message) {
424 return Output::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.queryBox"));
425 };
426 if (!finite(center) || !finite(halfExtent) || glm::any(glm::lessThan(halfExtent, glm::vec3(0.f))) ||
427 glm::any(glm::greaterThan(halfExtent, glm::vec3(10000.f))) || !finite(rotation) ||
428 std::abs(glm::dot(rotation, rotation) - 1.f) > .001f || !range(contactOffset, 0.f, 10000.f) ||
429 !range(maxDistance, 0.f, 10000.f) || maxHits == 0 || maxHits > 4096 || phaseMask == 0 ||
430 (phaseMask & ~0x0fu) != 0 || !validFilter(collisionFilter))
431 return failure("Invalid box query, transform, distance, hit limit, phase mask or collision filter");
432 if (impl_->state.size() > 65536) return failure("Box query exceeds 65536-particle work budget");
433 const auto boxRotation = glm::mat3_cast(glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z)));
434 const auto inverseRotation = glm::transpose(boxRotation);
435 struct Farther {
436 bool operator()(const VolumeFluidDistanceHit& a, const VolumeFluidDistanceHit& b) const {
437 if (a.distance != b.distance) return a.distance < b.distance;
438 return a.particleIndex < b.particleIndex;
439 }
440 };
441 std::priority_queue<VolumeFluidDistanceHit, std::vector<VolumeFluidDistanceHit>, Farther> nearest;
442 for (size_t i = 0; i < impl_->state.size(); ++i) {
443 const auto& particle = impl_->state[i];
444 const unsigned phase = unsigned(particle.material.phase);
445 if ((phaseMask & (1u << phase)) == 0 || !filtersMatch(collisionFilter, particle.collisionFilter)) continue;
446 const auto local = inverseRotation * (particle.position - center);
447 const auto faceDistance = halfExtent - glm::abs(local);
448 glm::vec3 localPoint, localNormal(0.f);
449 if (glm::all(glm::greaterThanEqual(faceDistance, glm::vec3(0.f)))) {
450 int axis = 0;
451 if (faceDistance.y < faceDistance[axis]) axis = 1;
452 if (faceDistance.z < faceDistance[axis]) axis = 2;
453 localNormal[axis] = local[axis] > 0.f ? 1.f : -1.f;
454 localPoint = local;
455 localPoint[axis] = halfExtent[axis] * localNormal[axis];
456 } else {
457 localPoint = glm::clamp(local, -halfExtent, halfExtent);
458 localNormal = glm::normalize(local - localPoint);
459 }
460 const auto normal = boxRotation * localNormal;
461 const auto queryPoint = center + boxRotation * (localPoint + localNormal * contactOffset);
462 const float distance = glm::dot(particle.position - queryPoint, normal) - ellipsoidRadius(particle, normal);
463 if (distance > maxDistance) continue;
464 VolumeFluidDistanceHit hit{unsigned(i), distance, queryPoint, normal, particle};
465 if (nearest.size() < maxHits)
466 nearest.push(std::move(hit));
467 else {
468 const auto& worst = nearest.top();
469 if (distance > worst.distance || (distance == worst.distance && i >= worst.particleIndex)) continue;
470 nearest.pop();
471 nearest.push(std::move(hit));
472 }
473 }
474 std::vector<VolumeFluidDistanceHit> hits(nearest.size());
475 for (size_t i = hits.size(); i > 0; --i) {
476 hits[i - 1] = nearest.top();
477 nearest.pop();
478 }
479 return Output::success(std::move(hits));
480}
481
482Result<std::vector<VolumeFluidQueryHit>> VolumeFluid::queryBatch(std::span<const VolumeFluidQueryShape> queries,
483 unsigned maxHitsPerQuery) const {
485 const auto failure = [](const char* message) {
486 return Output::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, message, "fluids.volume.queryBatch"));
487 };
488 if (queries.size() > 256 || maxHitsPerQuery == 0 || maxHitsPerQuery > 4096)
489 return failure("Invalid batch query count or hit limit");
490 if (impl_->state.size() > 65536) return failure("Batch query exceeds 65536-particle source budget");
491 if (!queries.empty() && impl_->state.size() > 4000000u / queries.size())
492 return failure("Batch query exceeds four-million candidate budget");
493 for (const auto& query : queries) {
494 if (unsigned(query.type) > unsigned(VolumeFluidQueryType::Ray) || !finite(query.center) ||
495 !finite(query.size) || !range(query.contactOffset, 0.f, 10000.f) ||
496 !range(query.maxDistance, 0.f, 10000.f) || query.phaseMask == 0 || (query.phaseMask & ~0x0fu) != 0 ||
497 !validFilter(query.collisionFilter))
498 return failure("Invalid batch query shape, distance or phase mask");
499 if (query.type == VolumeFluidQueryType::Sphere &&
500 (!range(query.size.x, 0.f, 10000.f) || query.size.y != 0.f || query.size.z != 0.f))
501 return failure("Sphere batch size must contain radius in x only");
502 if (query.type == VolumeFluidQueryType::Box &&
503 (glm::any(glm::lessThan(query.size, glm::vec3(0.f))) ||
504 glm::any(glm::greaterThan(query.size, glm::vec3(20000.f))) || !finite(query.rotation) ||
505 std::abs(glm::dot(query.rotation, query.rotation) - 1.f) > .001f))
506 return failure("Invalid batch box size or rotation");
507 if (query.type == VolumeFluidQueryType::Ray && glm::length(query.size - query.center) <= 1e-7f)
508 return failure("Batch ray segment must be nonzero");
509 }
510 std::vector<VolumeFluidQueryHit> output;
511 output.reserve(std::min<size_t>(queries.size() * size_t(maxHitsPerQuery), 4000000u));
512 for (size_t queryIndex = 0; queryIndex < queries.size(); ++queryIndex) {
513 const auto& query = queries[queryIndex];
514 if (query.type == VolumeFluidQueryType::Ray) {
515 const auto segment = query.size - query.center;
516 const float segmentLength = glm::length(segment);
517 const auto direction = segment / segmentLength;
518 struct Farther {
519 bool operator()(const VolumeFluidQueryHit& a, const VolumeFluidQueryHit& b) const {
520 if (a.distance != b.distance) return a.distance < b.distance;
521 return a.particleIndex < b.particleIndex;
522 }
523 };
524 std::priority_queue<VolumeFluidQueryHit, std::vector<VolumeFluidQueryHit>, Farther> nearest;
525 for (size_t particleIndex = 0; particleIndex < impl_->state.size(); ++particleIndex) {
526 const auto& particle = impl_->state[particleIndex];
527 if ((query.phaseMask & (1u << unsigned(particle.material.phase))) == 0 ||
528 !filtersMatch(query.collisionFilter, particle.collisionFilter))
529 continue;
530 const auto fromStart = particle.position - query.center;
531 const float along = std::clamp(glm::dot(fromStart, direction), 0.f, segmentLength);
532 auto centerLine = query.center + direction * along;
533 auto centerToParticle = particle.position - centerLine;
534 float centerDistance = glm::length(centerToParticle);
535 auto normal = centerDistance > 1e-7f ? centerToParticle / centerDistance : -direction;
536 float distance = centerDistance - ellipsoidRadius(particle, normal) - query.contactOffset;
537 auto queryPoint = centerLine + normal * query.contactOffset;
538 float entry = 0.f;
539 glm::vec3 hitNormal;
540 if (rayEllipsoid(particle, query.center, direction, segmentLength, entry, hitNormal)) {
541 centerLine = query.center + direction * entry;
542 normal = -hitNormal;
543 queryPoint = centerLine + normal * query.contactOffset;
544 distance = -query.contactOffset;
545 }
546 if (distance > query.maxDistance) continue;
548 unsigned(queryIndex), unsigned(particleIndex), distance, queryPoint, normal, particle};
549 if (nearest.size() < maxHitsPerQuery)
550 nearest.push(std::move(hit));
551 else if (distance < nearest.top().distance ||
552 (distance == nearest.top().distance && particleIndex < nearest.top().particleIndex)) {
553 nearest.pop();
554 nearest.push(std::move(hit));
555 }
556 }
557 std::vector<VolumeFluidQueryHit> hits(nearest.size());
558 for (size_t i = hits.size(); i > 0; --i) {
559 hits[i - 1] = nearest.top();
560 nearest.pop();
561 }
562 output.insert(output.end(), std::make_move_iterator(hits.begin()), std::make_move_iterator(hits.end()));
563 } else {
565 query.type == VolumeFluidQueryType::Sphere
566 ? querySphere(query.center, query.size.x, query.contactOffset, query.maxDistance, maxHitsPerQuery,
567 query.phaseMask, query.collisionFilter)
568 : queryBox(query.center, query.size * .5f, query.rotation, query.contactOffset, query.maxDistance,
569 maxHitsPerQuery, query.phaseMask, query.collisionFilter);
570 if (!hits) return failure("Validated batch distance query failed");
571 for (const auto& hit : hits.value())
572 output.push_back(
573 {unsigned(queryIndex), hit.particleIndex, hit.distance, hit.queryPoint, hit.normal, hit.particle});
574 }
575 }
576 return Output::success(std::move(output));
577}
578
579Result<std::vector<unsigned>> VolumeFluid::applyQueryColors(std::span<const VolumeFluidQueryShape> queries,
580 std::span<const glm::vec4> overlapColors,
581 glm::vec4 baseColor, unsigned maxHitsPerQuery) {
582 using Output = Result<std::vector<unsigned>>;
583 const auto validColor = [](glm::vec4 color) {
584 return range(color.x, 0.f, 1.f) && range(color.y, 0.f, 1.f) && range(color.z, 0.f, 1.f) &&
585 range(color.w, 0.f, 1.f);
586 };
587 if (queries.size() != overlapColors.size() || !validColor(baseColor) ||
588 std::any_of(overlapColors.begin(), overlapColors.end(), [&](glm::vec4 color) { return !validColor(color); }))
590 "Query colors must be finite RGBA values with one color per query",
591 "fluids.volume.applyQueryColors"));
592 auto hits = queryBatch(queries, maxHitsPerQuery);
593 if (!hits) return Output::failure(hits.status());
594 std::vector<unsigned> counts(queries.size(), 0);
595 for (auto& particle : impl_->state) particle.color = baseColor;
596 for (const auto& hit : hits.value())
597 if (hit.distance < 0.f) {
598 impl_->state[hit.particleIndex].color = overlapColors[hit.queryIndex];
599 ++counts[hit.queryIndex];
600 }
601 return Output::success(std::move(counts));
602}
603
604Result<std::vector<unsigned>> VolumeFluid::applyQueryColorsPreservingOutside(
605 std::span<const VolumeFluidQueryShape> queries, std::span<const glm::vec4> overlapColors,
606 unsigned maxHitsPerQuery) {
607 using Output = Result<std::vector<unsigned>>;
608 const auto validColor = [](glm::vec4 color) {
609 return range(color.x, 0.f, 1.f) && range(color.y, 0.f, 1.f) && range(color.z, 0.f, 1.f) &&
610 range(color.w, 0.f, 1.f);
611 };
612 if (queries.size() != overlapColors.size() ||
613 std::any_of(overlapColors.begin(), overlapColors.end(), [&](glm::vec4 color) { return !validColor(color); }))
615 "Query colors must be finite RGBA values with one color per query",
616 "fluids.volume.applyQueryColorsPreservingOutside"));
617 auto hits = queryBatch(queries, maxHitsPerQuery);
618 if (!hits) return Output::failure(hits.status());
619 std::vector<unsigned> counts(queries.size(), 0);
620 for (const auto& hit : hits.value())
621 if (hit.distance < 0.f) {
622 impl_->state[hit.particleIndex].color = overlapColors[hit.queryIndex];
623 ++counts[hit.queryIndex];
624 }
625 return Output::success(std::move(counts));
626}
627
628Result<std::vector<VolumeFluidSimplexHit>> VolumeFluid::querySimplexes(std::span<const VolumeFluidQueryShape> queries,
629 unsigned maxHitsPerQuery) const {
630 return detail::querySimplexes(impl_->state, impl_->simplexes, queries, maxHitsPerQuery);
631}
632
633} // namespace eve::fluids
double value
std::string output
std::string terrain
eve::EntitySpatialPose pose
float phase
Definition CaveMesh.cpp:58
glm::vec4 p[6]
std::string label
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
float maximum[3]
float minimum[3]
std::vector< std::uint32_t > indices
std::int32_t c
int h
std::string local
size_t offset
Range range
std::array< float, 4 > rotation
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
Texture * normal
bool finite
std::string error
Definition Package.cpp:60
World3D * world
float radius
bool hit
float begin
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
bool found
uint32_t index
glm::vec3 point
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
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
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
Result< unsigned > bindDynamicParticles(std::span< const unsigned > particleIndices, unsigned colliderLabel, float compliance, float breakThreshold, bool constrainOrientation=false)
Installs Fluid3D-style compliant dynamic pins for a particle group.
std::span< const VolumeFluidAttachmentReaction > attachmentReactions() const
Borrows attachment-constraint reactions from the last completed step.
Result< void > setHeightFieldColliders(std::span< const VolumeFluidHeightFieldCollider > colliders)
Atomically replaces owned Fluid3D-compatible regular height-field colliders.
Result< unsigned > unbindStaticParticles(std::span< const unsigned > particleIndices)
Removes anchors for the supplied unique current indices, returning the number removed atomically.
Result< std::vector< VolumeFluidDistanceHit > > queryBox(glm::vec3 center, glm::vec3 halfExtent, glm::vec4 rotation, float contactOffset, float maxDistance, unsigned maxHits=64, unsigned phaseMask=0x0f, unsigned collisionFilter=0xffffffffu) const
Returns nearest signed distances between an oriented box and particle surfaces.
std::vector< VolumeFluidContact > contacts() const
Returns owning events from the last completed step; no borrowed world pointers are stored.
Result< std::vector< VolumeFluidParticle > > overlap(glm::vec3 center, float radius) const
Queries a sphere, returning particle values; invalid query input returns a diagnostic.
Result< std::vector< VolumeFluidQueryHit > > queryBatch(std::span< const VolumeFluidQueryShape > queries, unsigned maxHitsPerQuery=64) const
Evaluates a bounded mixed batch of sphere, box and ray queries.
Result< std::vector< VolumeFluidParticle > > overlapBox(glm::vec3 center, glm::vec3 halfExtent, glm::vec4 rotation) const
Queries particle centers inside an oriented box, including its boundary.
Result< std::vector< VolumeFluidDistanceHit > > querySphere(glm::vec3 center, float radius, float contactOffset, float maxDistance, unsigned maxHits=64, unsigned phaseMask=0x0f, unsigned collisionFilter=0xffffffffu) const
Returns nearest signed distances between a sphere and particle surfaces.
Result< unsigned > bindStaticParticles(std::span< const unsigned > particleIndices, unsigned colliderLabel, bool constrainOrientation=false)
Binds a particle group to an existing analytic collider in its current local frame.
Result< void > setColliders(std::span< const VolumeFluidCollider > colliders)
Atomically replaces owned obstacle samples; rejects invalid geometry and duplicate labels.
Result< void > setSdfColliders(std::span< const VolumeFluidSdfCollider > colliders)
Atomically replaces at most 16 owned mesh/SDF colliders; total samples are bounded to 4M.
Result< void > updateSdfColliderPoses(std::span< const VolumeFluidSdfPose > poses)
Atomically updates transforms of existing owned SDF colliders without copying distance samples.
Result< std::vector< VolumeFluidRayHit > > raycast(glm::vec3 origin, glm::vec3 direction, float maxDistance, unsigned maxHits=64, unsigned phaseMask=0x0f) const
Raycasts native oriented-ellipsoid particles and returns nearest hits first.
Result< void > setStitches(std::span< const VolumeFluidStitch > stitches)
Atomically replaces Fluid3DStitcher-compatible zero-distance constraints.
GLSL compute kernels for the GPU surface-flow solver.
Definition FluidTarget.h:12
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Owning signed surface-distance result for a native query shape.
Owning result from one entry in a mixed query batch.
Owning ray hit against one native oriented-ellipsoid volume-fluid particle.
glm::uvec4 counts
glm::vec4 color