183Result<void> VolumeFluid::stepWithThermalContacts(
float seconds,
unsigned substeps,
184 std::span<const VolumeFluidThermalRule> rules) {
186 if (!std::isfinite(seconds) || seconds <= 0.f || seconds > 1.f / 30.f || substeps == 0 || substeps > 32 ||
187 seconds /
float(substeps) > 1.f / 120.f)
188 return invalid(
"Invalid timestep or substep count");
189 if (rules.size() > 1024)
return invalid(
"Too many thermal rules");
190 auto& ordered = impl_->thermalRulesScratch;
191 ordered.assign(rules.begin(), rules.end());
192 std::sort(ordered.begin(), ordered.end(),
193 [](
const auto&
a,
const auto&
b) { return a.colliderLabel < b.colliderLabel; });
194 for (
size_t i = 0; i < ordered.size(); ++i) {
195 const auto& rule = ordered[i];
196 if (!
range(rule.rate, -1000000.f, 1000000.f) || !
range(rule.minimumViscosity, 0.f, 100.f) ||
197 !
range(rule.maximumViscosity, rule.minimumViscosity, 100.f) || !
range(rule.minimumCohesion, 0.f, 10.f) ||
198 !
range(rule.maximumCohesion, rule.minimumCohesion, 10.f) ||
199 (i > 0 && ordered[i - 1].colliderLabel == rule.colliderLabel) ||
200 std::none_of(impl_->colliders.begin(), impl_->colliders.end(),
201 [&](
const auto&
c) { return c.label == rule.colliderLabel; }))
202 return invalid(
"Invalid thermal rule or collider label");
204 auto& contactParticles = impl_->thermalContactParticles;
205 contactParticles.clear();
209 glm::quat startOrientation, targetOrientation;
210 glm::vec3 localPosition;
211 glm::quat localOrientation;
212 unsigned colliderLabel;
213 size_t colliderIndex;
214 glm::vec3 inertialReaction;
216 bool constrainOrientation;
218 float compliance, breakThreshold;
219 glm::vec3 constraintReaction{0.f};
220 glm::vec3 dynamicAngularReaction{0.f};
223 std::vector<Motion> motions;
224 if (!impl_->attachments.empty()) {
225 std::vector<std::pair<unsigned, size_t>> labels;
226 labels.reserve(impl_->colliders.size());
227 for (
size_t i = 0; i < impl_->colliders.size(); ++i) labels.emplace_back(impl_->colliders[i].label, i);
228 std::sort(labels.begin(), labels.end());
229 motions.reserve(impl_->attachments.size());
230 for (
const auto& attachment : impl_->attachments) {
231 const auto found = std::lower_bound(labels.begin(), labels.end(),
232 std::pair<unsigned, size_t>{attachment.colliderLabel, 0});
233 if (
found == labels.end() ||
found->first != attachment.colliderLabel)
234 return invalid(
"Missing solid attachment collider");
235 const auto target = impl_->colliders[
found->second].center +
236 impl_->colliderRotations[
found->second] * attachment.localPosition;
237 const auto start = impl_->state[attachment.particleIndex].position;
239 const auto& collider = impl_->colliders[
found->second];
240 const auto colliderQ =
241 glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
242 const auto localQ = glm::quat(attachment.localOrientation.w, attachment.localOrientation.x,
243 attachment.localOrientation.y, attachment.localOrientation.z);
244 const auto startQ = particleQuaternion(impl_->state[attachment.particleIndex]);
245 const auto targetQ = glm::normalize(colliderQ * localQ);
247 glm::any(glm::notEqual(impl_->constrain(
target),
target)))
248 return invalid(
"Solid attachment motion exceeds bounds or speed");
249 const auto& particle = impl_->state[attachment.particleIndex];
250 const float mass = impl_->volume * particle.material.density;
251 const auto unconstrainedVelocity =
252 particle.velocity + impl_->settings.gravity * particle.material.buoyancy * seconds;
253 const auto targetAngular = collider.angularVelocity;
254 const auto radii = particle.radii;
255 const glm::vec3 principal =
259 const auto orientation = glm::mat3_cast(startQ);
260 const auto angularReaction =
261 -orientation * glm::mat3(principal.x, 0.f, 0.f, 0.f, principal.y, 0.f, 0.f, 0.f, principal.z) *
262 glm::transpose(orientation) * (targetAngular - particle.angularVelocity);
264 {attachment.particleIndex,
start,
target, velocity, startQ, targetQ, attachment.localPosition, localQ,
265 attachment.colliderLabel,
found->second,
266 attachment.dynamic ? glm::vec3(0.f) : -mass * (velocity - unconstrainedVelocity),
267 attachment.constrainOrientation ? targetAngular : particle.angularVelocity,
268 !attachment.dynamic && attachment.constrainOrientation ? angularReaction : glm::vec3(0.f),
269 attachment.constrainOrientation, attachment.dynamic, attachment.compliance,
270 attachment.breakThreshold});
273 auto& colliderEnds = impl_->colliderPoseScratch;
274 auto& sdfColliderEnds = impl_->sdfColliderPoseScratch;
275 auto& heightFieldEnds = impl_->heightFieldPoseScratch;
276 colliderEnds = impl_->colliders;
277 sdfColliderEnds.resize(impl_->sdfColliders.size());
278 for (
size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
279 const auto& collider = impl_->sdfColliders[i];
280 sdfColliderEnds[i] = {collider.label, collider.position, collider.rotation,
281 collider.scale, collider.velocity, collider.angularVelocity};
283 heightFieldEnds.resize(impl_->heightFieldColliders.size());
284 for (
size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
285 const auto& collider = impl_->heightFieldColliders[i];
286 heightFieldEnds[i] = {collider.label, collider.position, collider.rotation, 1.f,
287 collider.velocity, collider.angularVelocity};
292 if (speed <= 1e-7f)
return end;
293 return glm::normalize(glm::angleAxis(-speed * seconds * (1.f - fraction),
angularVelocity / speed) *
end);
295 for (
size_t i = 0; i < sdfColliderEnds.size(); ++i)
296 if (impl_->sdfColliders[i].inverted) {
297 const auto&
pose = sdfColliderEnds[i];
298 const auto& sdf = impl_->sdfColliders[i].sdf;
299 const auto startRotation =
300 glm::mat3_cast(interpolatedRotation(
pose.rotation,
pose.angularVelocity, seconds, 0.f));
301 const auto startPosition =
pose.position -
pose.velocity * seconds;
302 const auto minimum = sdf.origin;
303 const auto maximum =
minimum + glm::vec3(sdf.dims - glm::ivec3(1)) * sdf.cellSize;
304 for (
unsigned corner = 0; corner < 8; ++corner) {
305 const glm::vec3
world{corner & 1 ? impl_->settings.maximum.x : impl_->settings.minimum.x,
306 corner & 2 ? impl_->settings.maximum.y : impl_->settings.minimum.y,
307 corner & 4 ? impl_->settings.maximum.z : impl_->settings.minimum.z};
308 const auto local = glm::transpose(startRotation) * (
world - startPosition) /
pose.scale;
310 return invalid(
"Moving inverted SDF domain must contain solver bounds for the complete step");
313 const auto restoreColliderEnds = [&]() {
314 impl_->colliders.swap(colliderEnds);
315 for (
size_t i = 0; i < sdfColliderEnds.size(); ++i) {
316 impl_->sdfColliders[i].position = sdfColliderEnds[i].position;
317 impl_->sdfColliders[i].rotation = sdfColliderEnds[i].rotation;
319 for (
size_t i = 0; i < heightFieldEnds.size(); ++i) {
320 impl_->heightFieldColliders[i].position = heightFieldEnds[i].position;
321 impl_->heightFieldColliders[i].rotation = heightFieldEnds[i].rotation;
323 impl_->colliderRotations.resize(impl_->colliders.size());
324 for (
size_t i = 0; i < impl_->colliders.size(); ++i) {
325 const auto&
r = impl_->colliders[i].rotation;
326 impl_->colliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(
r.w,
r.x,
r.y,
r.z)));
328 impl_->sdfColliderRotations.resize(impl_->sdfColliders.size());
329 for (
size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
330 const auto&
r = impl_->sdfColliders[i].rotation;
331 impl_->sdfColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(
r.w,
r.x,
r.y,
r.z)));
333 impl_->heightFieldColliderRotations.resize(impl_->heightFieldColliders.size());
334 for (
size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
335 const auto&
r = impl_->heightFieldColliders[i].rotation;
336 impl_->heightFieldColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(
r.w,
r.x,
r.y,
r.z)));
339 auto& original = impl_->rollbackState;
340 original = impl_->state;
341 impl_->renderPreviousScratch.resize(impl_->state.size());
342 for (
size_t i = 0; i < impl_->state.size(); ++i) impl_->renderPreviousScratch[i] = impl_->state[i].position;
343 auto& originalContacts = impl_->rollbackContacts;
344 originalContacts = impl_->contacts;
345 auto& originalAttachments = impl_->rollbackAttachments;
346 originalAttachments = impl_->attachments;
347 impl_->attachmentLabels.clear();
348 impl_->attachmentOrientationConstraints.clear();
349 impl_->attachmentStressImpulse.clear();
350 if (!motions.empty()) {
351 impl_->attachmentLabels.assign(impl_->state.size(), -1);
352 impl_->attachmentOrientationConstraints.assign(impl_->state.size(), uint8_t(0));
353 impl_->attachmentStressImpulse.assign(impl_->state.size(), glm::vec3(0.f));
354 for (
const auto& motion : motions) {
355 if (motion.dynamic)
continue;
356 impl_->attachmentLabels[motion.particle] = int(motion.colliderLabel);
357 impl_->attachmentOrientationConstraints[motion.particle] = motion.constrainOrientation ? 1 : 0;
360 impl_->contacts.clear();
361 for (
auto&
p : impl_->state)
362 if (
p.material.phase == VolumeFluidPhase::Solid) {
363 p.velocity = glm::vec3(0.f);
364 p.angularVelocity = glm::vec3(0.f);
366 for (
unsigned k = 0; k < substeps; ++k) {
367 const float fraction = float(k + 1) / float(substeps);
368 for (
size_t i = 0; i < colliderEnds.size(); ++i) {
369 const auto&
end = colliderEnds[i];
370 auto& sample = impl_->colliders[i];
371 sample.center =
end.center -
end.velocity * (seconds * (1.f - fraction));
372 const auto q = interpolatedRotation(
end.rotation,
end.angularVelocity, seconds, fraction);
373 sample.rotation = {
q.x,
q.y,
q.z,
q.w};
374 impl_->colliderRotations[i] = glm::mat3_cast(
q);
376 for (
size_t i = 0; i < sdfColliderEnds.size(); ++i) {
377 const auto&
end = sdfColliderEnds[i];
378 auto& sample = impl_->sdfColliders[i];
379 sample.position =
end.position -
end.velocity * (seconds * (1.f - fraction));
380 const auto q = interpolatedRotation(
end.rotation,
end.angularVelocity, seconds, fraction);
381 sample.rotation = {
q.x,
q.y,
q.z,
q.w};
382 impl_->sdfColliderRotations[i] = glm::mat3_cast(
q);
384 for (
size_t i = 0; i < heightFieldEnds.size(); ++i) {
385 const auto&
end = heightFieldEnds[i];
386 auto& sample = impl_->heightFieldColliders[i];
387 sample.position =
end.position -
end.velocity * (seconds * (1.f - fraction));
388 const auto q = interpolatedRotation(
end.rotation,
end.angularVelocity, seconds, fraction);
389 sample.rotation = {
q.x,
q.y,
q.z,
q.w};
390 impl_->heightFieldColliderRotations[i] = glm::mat3_cast(
q);
392 for (
const auto& motion : motions) {
393 if (motion.dynamic)
continue;
394 auto&
p = impl_->state[motion.particle];
395 p.position = glm::mix(motion.start, motion.target,
float(k + 1) /
float(substeps));
396 p.velocity =
p.material.phase == VolumeFluidPhase::Solid ? motion.velocity : glm::vec3(0.f);
397 if (motion.constrainOrientation) {
398 p.angularVelocity = motion.angularVelocity;
399 const auto orientation = glm::normalize(
400 glm::slerp(motion.startOrientation, motion.targetOrientation,
float(k + 1) /
float(substeps)));
401 p.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
404 if (motions.empty()) {
405 if (impl_->externalForces.empty())
406 impl_->substep<
false,
false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
408 impl_->substep<
false,
true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
409 }
else if (impl_->externalForces.empty()) {
410 impl_->substep<
true,
false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
412 impl_->substep<
true,
true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
414 if (!impl_->stitches.empty()) {
415 const float stitchDt = seconds / float(substeps);
416 impl_->stitchStart.resize(impl_->state.size());
417 impl_->stitchDelta.resize(impl_->state.size());
418 impl_->stitchCounts.resize(impl_->state.size());
419 impl_->stitchTouched.clear();
420 impl_->stitchTouched.reserve(impl_->stitches.size() * 2);
421 for (
const auto& stitch : impl_->stitches) {
422 impl_->stitchTouched.push_back(stitch.particleIndex1);
423 impl_->stitchTouched.push_back(stitch.particleIndex2);
424 impl_->stitchStart[stitch.particleIndex1] = impl_->state[stitch.particleIndex1].position;
425 impl_->stitchStart[stitch.particleIndex2] = impl_->state[stitch.particleIndex2].position;
427 std::sort(impl_->stitchTouched.begin(), impl_->stitchTouched.end());
428 impl_->stitchTouched.erase(std::unique(impl_->stitchTouched.begin(), impl_->stitchTouched.end()),
429 impl_->stitchTouched.end());
430 impl_->stitchLambdas.assign(impl_->stitches.size(), 0.f);
431 for (
unsigned iteration = 0; iteration < impl_->settings.iterations; ++iteration) {
432 for (
const unsigned index : impl_->stitchTouched) {
433 impl_->stitchDelta[
index] = glm::vec3(0.f);
434 impl_->stitchCounts[
index] = 0;
436 for (
size_t stitchIndex = 0; stitchIndex < impl_->stitches.size(); ++stitchIndex) {
437 const auto& stitch = impl_->stitches[stitchIndex];
438 const auto fixed = [&](
unsigned index) {
439 return impl_->state[
index].material.phase == VolumeFluidPhase::Solid ||
440 impl_->grabberLabels[
index] != Impl::noGrabber ||
441 (!impl_->attachmentLabels.empty() && impl_->attachmentLabels[
index] >= 0);
443 const float w1 = fixed(stitch.particleIndex1)
445 : 1.f / (impl_->volume * impl_->state[stitch.particleIndex1].material.density);
446 const float w2 = fixed(stitch.particleIndex2)
448 : 1.f / (impl_->volume * impl_->state[stitch.particleIndex2].material.density);
450 impl_->state[stitch.particleIndex1].position - impl_->state[stitch.particleIndex2].position;
452 const float alpha = stitch.compliance / (stitchDt * stitchDt);
453 const float deltaLambda = (-
length - alpha * impl_->stitchLambdas[stitchIndex]) /
454 (w1 + w2 + alpha + std::numeric_limits<float>::epsilon());
455 const auto delta = deltaLambda *
distance / (
length + std::numeric_limits<float>::epsilon());
456 impl_->stitchDelta[stitch.particleIndex1] += delta * w1;
457 impl_->stitchDelta[stitch.particleIndex2] -= delta * w2;
458 ++impl_->stitchCounts[stitch.particleIndex1];
459 ++impl_->stitchCounts[stitch.particleIndex2];
460 impl_->stitchLambdas[stitchIndex] += deltaLambda;
462 for (
const unsigned index : impl_->stitchTouched)
463 if (impl_->stitchCounts[
index] != 0)
464 impl_->state[
index].position += impl_->stitchDelta[
index] / float(impl_->stitchCounts[
index]);
466 for (
const unsigned index : impl_->stitchTouched)
467 if (impl_->stitchCounts[
index] != 0)
468 impl_->state[
index].velocity +=
469 (impl_->state[
index].position - impl_->stitchStart[
index]) / stitchDt;
471 const float substepSeconds = seconds / float(substeps);
472 for (
auto& motion : motions) {
473 if (!motion.dynamic || motion.broken)
continue;
474 auto& particle = impl_->state[motion.particle];
475 const auto& collider = impl_->colliders[motion.colliderIndex];
476 const auto&
rotation = impl_->colliderRotations[motion.colliderIndex];
477 const auto target = collider.center +
rotation * motion.localPosition;
478 const float mass = impl_->volume * particle.material.density;
479 const float inverseMass = 1.f / mass;
480 const float alpha = motion.compliance / (substepSeconds * substepSeconds);
481 const float response = inverseMass / (inverseMass + alpha);
482 const auto correction = (
target - particle.position) * response;
483 particle.position += correction;
484 particle.velocity += correction / substepSeconds;
485 motion.constraintReaction -= mass * correction / substepSeconds;
486 float force = mass * glm::length(correction) / (substepSeconds * substepSeconds);
488 if (motion.constrainOrientation) {
489 const auto colliderQ =
490 glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
491 const auto currentQ = particleQuaternion(particle);
492 const auto desiredQ = glm::normalize(colliderQ * motion.localOrientation);
493 const auto nextQ = glm::normalize(glm::slerp(currentQ, desiredQ, response));
494 auto deltaQ = glm::normalize(nextQ * glm::conjugate(currentQ));
495 if (deltaQ.w < 0.f) deltaQ = -deltaQ;
496 const float angle = 2.f * std::acos(std::clamp(deltaQ.w, -1.f, 1.f));
497 glm::vec3 deltaAngular(0.f);
498 const float sine = std::sqrt(std::max(0.f, 1.f - deltaQ.w * deltaQ.w));
500 deltaAngular = glm::vec3(deltaQ.x, deltaQ.y, deltaQ.z) * (
angle / sine / substepSeconds);
501 particle.orientation = {nextQ.x, nextQ.y, nextQ.z, nextQ.w};
502 particle.angularVelocity += deltaAngular;
503 const auto radii = particle.radii;
509 motion.dynamicAngularReaction -= deltaAngular *
inertia;
511 if (force > motion.breakThreshold) motion.broken =
true;
513 for (
const auto&
p : impl_->state)
515 glm::length(
p.angularVelocity) > 1000.f || !
finite(
p.color) || !
finite(
p.data) ||
516 (
p.material.phase == VolumeFluidPhase::Solid &&
517 glm::any(glm::notEqual(impl_->constrain(
p.position),
p.position)))) {
518 impl_->state = original;
519 impl_->contacts = originalContacts;
520 impl_->attachments = originalAttachments;
521 restoreColliderEnds();
523 DiagnosticCode::Failed,
"Nonfinite state or solid outside bounds; step rolled back",
524 "fluids.volume.step"));
527 restoreColliderEnds();
528 if (std::any_of(motions.begin(), motions.end(), [](
const auto& motion) { return motion.broken; })) {
529 std::erase_if(impl_->attachments, [&](
const auto& attachment) {
530 return std::any_of(motions.begin(), motions.end(), [&](const auto& motion) {
531 return motion.broken && motion.particle == attachment.particleIndex &&
532 motion.colliderLabel == attachment.colliderLabel;
537 auto propagated = impl_->propagateSolidification();
539 impl_->state = original;
540 impl_->contacts = originalContacts;
541 impl_->attachments = originalAttachments;
544 impl_->attachmentReactionScratch.clear();
545 impl_->attachmentReactionScratch.reserve(motions.size());
546 for (
const auto& motion : motions) {
548 motion.inertialReaction + motion.constraintReaction + impl_->attachmentStressImpulse[motion.particle];
549 const auto angularImpulse = motion.angularReaction + motion.dynamicAngularReaction;
550 if (glm::dot(impulse, impulse) > 0.f || glm::dot(angularImpulse, angularImpulse) > 0.f)
551 impl_->attachmentReactionScratch.push_back({motion.colliderLabel, motion.target, impulse, angularImpulse});
553 impl_->attachmentReactions.swap(impl_->attachmentReactionScratch);
554 auto& hits = impl_->thermalHitScratch;
556 hits.reserve(contactParticles.size());
557 for (
size_t i = 0; i < contactParticles.size(); ++i)
558 hits.push_back((uint64_t(impl_->contacts[i].colliderLabel) << 32u) | uint64_t(contactParticles[i]));
559 std::sort(hits.begin(), hits.end());
560 hits.erase(std::unique(hits.begin(), hits.end()), hits.end());
561 for (
const uint64_t
hit : hits) {
562 const unsigned label = unsigned(
hit >> 32u);
563 const auto rule = std::lower_bound(ordered.begin(), ordered.end(),
label,
564 [](
const auto& item,
unsigned value) { return item.colliderLabel < value; });
565 if (rule == ordered.end() || rule->colliderLabel !=
label)
continue;
566 auto& data = impl_->state[size_t(
hit & 0xFFFFFFFFull)].data;
567 data.x = std::clamp(data.x + rule->rate * seconds, rule->minimumViscosity, rule->maximumViscosity);
568 data.y = std::clamp(data.y + rule->rate * seconds, rule->minimumCohesion, rule->maximumCohesion);
571 impl_->next.resize(impl_->state.size());
572 impl_->grabberLabelScratch.resize(impl_->state.size());
573 impl_->grabberLocalScratch.resize(impl_->state.size());
574 size_t remaining = 0;
575 for (
size_t i = 0; i < impl_->state.size(); ++i) {
576 auto&
p = impl_->state[i];
577 const bool expires =
p.life > 0.f && (
p.life -= seconds) <= 0.f;
578 impl_->next[i] = expires ? -1 : int(remaining);
579 if (expires) impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed,
unsigned(i),
p);
581 if (remaining != i) impl_->state[remaining] =
p;
582 impl_->renderPreviousScratch[remaining] = impl_->renderPreviousScratch[i];
583 impl_->grabberLabelScratch[remaining] = impl_->grabberLabels[i];
584 impl_->grabberLocalScratch[remaining] = impl_->grabberLocalPositions[i];
588 impl_->state.resize(remaining);
589 impl_->renderPreviousScratch.resize(remaining);
590 impl_->renderPrevious.swap(impl_->renderPreviousScratch);
591 impl_->grabberLabelScratch.resize(remaining);
592 impl_->grabberLabels.swap(impl_->grabberLabelScratch);
593 impl_->grabberLocalScratch.resize(remaining);
594 impl_->grabberLocalPositions.swap(impl_->grabberLocalScratch);
595 std::erase_if(impl_->attachments, [&](
auto&
a) {
596 const int mapped = impl_->next[a.particleIndex];
597 if (mapped < 0) return true;
598 a.particleIndex = unsigned(mapped);
601 std::erase_if(impl_->stitches, [&](
auto& stitch) {
602 const int first = impl_->next[stitch.particleIndex1];
603 const int second = impl_->next[stitch.particleIndex2];
604 if (first < 0 || second < 0) return true;
605 stitch.particleIndex1 = unsigned(first);
606 stitch.particleIndex2 = unsigned(second);
609 std::erase_if(impl_->simplexes, [&](
auto& simplex) {
610 for (unsigned i = 0; i < simplex.size; ++i) {
611 const int mapped = impl_->next[simplex.particleIndices[i]];
612 if (mapped < 0) return true;
613 simplex.particleIndices[i] = unsigned(mapped);
617 impl_->winds.clear();
618 impl_->externalForces.clear();
619 return Result<void>::success();