1#include "fluids/VolumeFluidInternal.inc"
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) {
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");
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)
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());
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();
49 bool constrainOrientation) {
50 return bindParticles(particleIndices, colliderLabel, constrainOrientation,
false, 0.f, 1e12f);
54 float compliance,
float breakThreshold,
bool constrainOrientation) {
55 return bindParticles(particleIndices, colliderLabel, constrainOrientation,
true, compliance, breakThreshold);
58Result<unsigned> VolumeFluid::bindParticles(std::span<const unsigned> particleIndices,
unsigned colliderLabel,
59 bool constrainOrientation,
bool dynamic,
float compliance,
60 float breakThreshold) {
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))
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,
94 inverseRotation * (impl_->state[
index].position - collider->center),
95 {localQ.x, localQ.y, localQ.z, localQ.w},
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()));
112 if (particleIndices.empty() || particleIndices.size() > impl_->state.size())
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);
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()));
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);
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);
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);
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) {
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");
183 glm::mat3_cast(glm::normalize(glm::quat(
c.rotation.w,
c.rotation.x,
c.rotation.y,
c.rotation.z)));
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};
193 return invalid(
"Inverted SDF domain must contain solver bounds");
198 std::vector<VolumeFluidSdfCollider> replacement(
input.begin(),
input.end());
199 impl_->sdfColliders.swap(replacement);
200 impl_->sdfColliderRotations.swap(rotations);
201 impl_->releaseMissingGrabbers();
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) {
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");
233 glm::mat3_cast(glm::normalize(glm::quat(
c.rotation.w,
c.rotation.x,
c.rotation.y,
c.rotation.z))));
235 std::vector<VolumeFluidHeightFieldCollider> replacement(
input.begin(),
input.end());
236 impl_->heightFieldColliders.swap(replacement);
237 impl_->heightFieldColliderRotations.swap(rotations);
238 impl_->releaseMissingGrabbers();
243 if (poses.size() > 16)
return invalid(
"Too many SDF collider pose updates");
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];
251 std::abs(glm::dot(
pose.rotation,
pose.rotation) - 1.f) > .001f || !
range(
pose.scale, .0001f, 10000.f) ||
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) {
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};
272 return invalid(
"Updated inverted SDF domain must contain solver bounds");
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];
294 return impl_->attachmentReactions;
301 std::vector<VolumeFluidParticle> hits;
302 for (
const auto&
p : impl_->state)
303 if (glm::length(
p.position -
center) <=
radius) hits.push_back(
p);
314 const auto inverseRotation =
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);
325 unsigned maxHits,
unsigned phaseMask)
const {
327 const auto failure = [](
const char*
message) {
330 const float directionLength = glm::length(
direction);
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");
339 if (
a.distance !=
b.distance)
return a.distance <
b.distance;
340 return a.particleIndex <
b.particleIndex;
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;
353 if (nearest.size() < maxHits)
354 nearest.push(std::move(
hit));
356 const auto& worst = nearest.top();
357 if (
distance > worst.distance || (
distance == worst.distance && i >= worst.particleIndex))
continue;
359 nearest.push(std::move(
hit));
362 std::vector<VolumeFluidRayHit> hits(nearest.size());
363 for (
size_t i = hits.size(); i > 0; --i) {
364 hits[i - 1] = nearest.top();
367 return Output::success(std::move(hits));
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(
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");
386 if (
a.distance !=
b.distance)
return a.distance <
b.distance;
387 return a.particleIndex <
b.particleIndex;
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;
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;
401 if (nearest.size() < maxHits)
402 nearest.push(std::move(
hit));
404 const auto& worst = nearest.top();
405 if (
distance > worst.distance || (
distance == worst.distance && i >= worst.particleIndex))
continue;
407 nearest.push(std::move(
hit));
410 std::vector<VolumeFluidDistanceHit> hits(nearest.size());
411 for (
size_t i = hits.size(); i > 0; --i) {
412 hits[i - 1] = nearest.top();
415 return Output::success(std::move(hits));
419 glm::vec4
rotation,
float contactOffset,
420 float maxDistance,
unsigned maxHits,
421 unsigned phaseMask,
unsigned collisionFilter)
const {
423 const auto failure = [](
const char*
message) {
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");
434 const auto inverseRotation = glm::transpose(boxRotation);
437 if (
a.distance !=
b.distance)
return a.distance <
b.distance;
438 return a.particleIndex <
b.particleIndex;
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);
448 glm::vec3 localPoint, localNormal(0.f);
449 if (glm::all(glm::greaterThanEqual(faceDistance, glm::vec3(0.f)))) {
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;
455 localPoint[axis] =
halfExtent[axis] * localNormal[axis];
458 localNormal = glm::normalize(
local - localPoint);
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;
465 if (nearest.size() < maxHits)
466 nearest.push(std::move(
hit));
468 const auto& worst = nearest.top();
469 if (
distance > worst.distance || (
distance == worst.distance && i >= worst.particleIndex))
continue;
471 nearest.push(std::move(
hit));
474 std::vector<VolumeFluidDistanceHit> hits(nearest.size());
475 for (
size_t i = hits.size(); i > 0; --i) {
476 hits[i - 1] = nearest.top();
479 return Output::success(std::move(hits));
483 unsigned maxHitsPerQuery)
const {
485 const auto failure = [](
const char*
message) {
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) {
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");
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");
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");
508 return failure(
"Batch ray segment must be nonzero");
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];
515 const auto segment = query.size - query.center;
516 const float segmentLength = glm::length(segment);
517 const auto direction = segment / segmentLength;
520 if (
a.distance !=
b.distance)
return a.distance <
b.distance;
521 return a.particleIndex <
b.particleIndex;
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))
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;
540 if (rayEllipsoid(particle, query.center,
direction, segmentLength, entry, hitNormal)) {
541 centerLine = query.center +
direction * entry;
543 queryPoint = centerLine +
normal * query.contactOffset;
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)) {
554 nearest.push(std::move(
hit));
557 std::vector<VolumeFluidQueryHit> hits(nearest.size());
558 for (
size_t i = hits.size(); i > 0; --i) {
559 hits[i - 1] = nearest.top();
562 output.insert(
output.end(), std::make_move_iterator(hits.begin()), std::make_move_iterator(hits.end()));
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())
573 {
unsigned(queryIndex),
hit.particleIndex,
hit.distance,
hit.queryPoint,
hit.normal,
hit.particle});
576 return Output::success(std::move(
output));
580 std::span<const glm::vec4> overlapColors,
581 glm::vec4 baseColor,
unsigned maxHitsPerQuery) {
583 const auto validColor = [](glm::vec4
color) {
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];
601 return Output::success(std::move(
counts));
605 std::span<const VolumeFluidQueryShape> queries, std::span<const glm::vec4> overlapColors,
606 unsigned maxHitsPerQuery) {
608 const auto validColor = [](glm::vec4
color) {
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];
625 return Output::success(std::move(
counts));
629 unsigned maxHitsPerQuery)
const {
630 return detail::querySimplexes(impl_->state, impl_->simplexes, queries, maxHitsPerQuery);
eve::EntitySpatialPose pose
std::vector< std::uint32_t > indices
std::array< float, 4 > rotation
RoadLaneDirection direction
A structured explanation of a failed, degraded, or noteworthy result.
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
const T & value() const &
Borrow the value from a const lvalue after checking success.
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.
DiagnosticCode
Stable machine-readable diagnostic codes.
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.